From 828d30010eb72f48a3340e524d09356104f91cc8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Apr 2026 16:21:45 +0000 Subject: [PATCH 01/23] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 43e1b436d..45404d99a 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java - 0.2.1-java.1 + 0.2.2-java.1-SNAPSHOT jar GitHub Copilot SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk-java.git scm:git:https://github.com/github/copilot-sdk-java.git https://github.com/github/copilot-sdk-java - v0.2.1-java.1 + HEAD From 3862fdd97a20a6e763efa7873ce426ae45bc4bf4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:29:22 +0000 Subject: [PATCH 02/23] Initial plan From be757e1421924626969ca7dc4df9bd9796aae923 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:36:18 +0000 Subject: [PATCH 03/23] Fix weekly upstream sync agentic workflow prompt Rewrite the prompt from documentation-style to clear imperative instructions. The agent previously misinterpreted the documentation as instructions to perform the merge itself (invoking the agentic-merge-upstream skill), then failed because it couldn't push code from the sandbox. The new prompt: - Gives explicit step-by-step instructions - Provides bash commands for upstream change detection - Directs the agent to use safe-output tools (create_issue, close_issue, assign_to_agent, noop) - Explicitly prohibits invoking skills, editing files, or pushing code Only the markdown content changed; frontmatter is unchanged so no lock file recompilation is needed. Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/945e9454-9363-45bc-b281-0fe3d6c87019 Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .github/workflows/weekly-upstream-sync.md | 134 ++++++++-------------- 1 file changed, 50 insertions(+), 84 deletions(-) diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md index 641f29301..0aaff8d1e 100644 --- a/.github/workflows/weekly-upstream-sync.md +++ b/.github/workflows/weekly-upstream-sync.md @@ -41,111 +41,77 @@ safe-outputs: noop: report-as-issue: false --- -# Weekly Upstream Sync Agentic Workflow -This document describes the `weekly-upstream-sync.yml` GitHub Actions workflow, which automates the detection of new changes in the official [Copilot SDK](https://github.com/github/copilot-sdk) and delegates the merge work to the Copilot coding agent. +# Weekly Upstream Sync -## Overview +You are an automation agent that detects new upstream changes and creates GitHub issues. You do **NOT** perform any code merges, edits, or pushes. Do **NOT** invoke any skills (especially `agentic-merge-upstream`). Your only job is to check for changes and use safe-output tools to create or close issues. -The workflow runs on a **weekly schedule** (every Monday at 10:00 UTC) and can also be triggered manually. It does **not** perform the actual merge — instead, it detects upstream changes and creates a GitHub issue assigned to `copilot`, instructing the agent to follow the [agentic-merge-upstream](../prompts/agentic-merge-upstream.prompt.md) prompt to port the changes. +## Instructions -The agent must also create the Pull Request with the label `upstream-sync`. This allows the workflow to track the merge progress and avoid creating duplicate issues if the agent is still working on a previous sync. +Follow these steps exactly: -## Trigger +### Step 1: Read `.lastmerge` -| Trigger | Schedule | -|---|---| -| `schedule` | Every Monday at 10:00 UTC (`0 10 * * 1`) | -| `workflow_dispatch` | Manual trigger from the Actions tab | +Read the file `.lastmerge` in the repository root. It contains the SHA of the last upstream commit that was merged into this Java SDK. -## Workflow Steps +### Step 2: Check for upstream changes -### 1. Checkout repository +Clone the upstream repository and compare commits: -Checks out the repo to read the `.lastmerge` file, which contains the SHA of the last upstream commit that was merged into the Java SDK. - -### 2. Check for upstream changes - -- Reads the last merged commit hash from `.lastmerge` -- Clones the upstream `github/copilot-sdk` repository -- Compares `.lastmerge` against upstream `HEAD` -- If they match: sets `has_changes=false` -- If they differ: counts new commits, generates a summary (up to 20 most recent), and sets outputs (`commit_count`, `upstream_head`, `last_merge`, `summary`) - -### 3. Close previous upstream-sync issues (when changes found) - -**Condition:** `has_changes == true` +```bash +LAST_MERGE=$(cat .lastmerge) +git clone --quiet https://github.com/github/copilot-sdk.git /tmp/gh-aw/agent/upstream +cd /tmp/gh-aw/agent/upstream +UPSTREAM_HEAD=$(git rev-parse HEAD) +``` -Before creating a new issue, closes any existing open issues with the `upstream-sync` label. This prevents stale issues from accumulating when previous sync attempts were incomplete or superseded. Each closed issue receives a comment explaining it was superseded. +If `LAST_MERGE` equals `UPSTREAM_HEAD`, there are **no new changes**. Go to Step 3a. -### 4. Close stale upstream-sync issues (when no changes found) +If they differ, count the new commits and generate a summary: -**Condition:** `has_changes == false` +```bash +COMMIT_COUNT=$(git rev-list --count "$LAST_MERGE".."$UPSTREAM_HEAD") +SUMMARY=$(git log --oneline "$LAST_MERGE".."$UPSTREAM_HEAD" | head -20) +``` -If the upstream is already up to date, closes any lingering open `upstream-sync` issues with a comment noting that no changes were detected. This handles the case where a previous issue was created but the changes were merged manually (updating `.lastmerge`) before the agent completed. +Go to Step 3b. -### 5. Create issue and assign to Copilot +### Step 3a: No changes detected -**Condition:** `has_changes == true` +1. Search for any open issues with the `upstream-sync` label using the GitHub MCP tools. +2. If there are open `upstream-sync` issues, close each one using the `close_issue` safe-output tool with a comment: "No new upstream changes detected. The Java SDK is up to date. Closing." +3. Call the `noop` safe-output tool with message: "No new upstream changes since last merge ()." +4. **Stop here.** Do not proceed further. -Creates a new GitHub issue with: +### Step 3b: Changes detected -- **Title:** `Upstream sync: N new commits (YYYY-MM-DD)` -- **Label:** `upstream-sync` -- **Assignee:** `copilot` -- **Body:** Contains commit count, commit range links, a summary of recent commits, and a link to the merge prompt +1. Search for any open issues with the `upstream-sync` label using the GitHub MCP tools. +2. Close each existing open `upstream-sync` issue using the `close_issue` safe-output tool with a comment: "Superseded by a newer upstream sync check." +3. Create a new issue using the `create_issue` safe-output tool with: + - **Title:** `Upstream sync: new commits ()` + - **Body:** Include the following information: + ``` + ## Automated Upstream Sync -The Copilot coding agent picks up the issue, creates a branch and PR, then follows the merge prompt to port the changes. + There are **** new commits in the [official Copilot SDK](https://github.com/github/copilot-sdk) since the last merge. -### 6. Summary + - **Last merged commit:** [``](https://github.com/github/copilot-sdk/commit/) + - **Upstream HEAD:** [``](https://github.com/github/copilot-sdk/commit/) -Writes a GitHub Actions step summary with: + ### Recent upstream commits -- Whether changes were detected -- Commit count and range -- Recent upstream commits -- Link to the created issue (if any) + ``` + + ``` -## Flow Diagram + ### Instructions -``` -┌─────────────────────┐ -│ Schedule / Manual │ -└──────────┬──────────┘ - │ - ▼ -┌─────────────────────┐ -│ Read .lastmerge │ -│ Clone upstream SDK │ -│ Compare commits │ -└──────────┬──────────┘ - │ - ┌─────┴─────┐ - │ │ - changes? no changes - │ │ - ▼ ▼ -┌──────────┐ ┌──────────────────┐ -│ Close old│ │ Close stale │ -│ issues │ │ issues │ -└────┬─────┘ └──────────────────┘ - │ - ▼ -┌──────────────────────────┐ -│ Create issue assigned to │ -│ copilot │ -└──────────────────────────┘ - │ - ▼ -┌──────────────────────────┐ -│ Agent follows prompt to │ -│ port changes → PR │ -└──────────────────────────┘ -``` + Follow the [agentic-merge-upstream](.github/prompts/agentic-merge-upstream.prompt.md) prompt to port these changes to the Java SDK. + ``` +4. After creating the issue, use the `assign_to_agent` safe-output tool to assign Copilot to the newly created issue. -## Related Files +## Important constraints -| File | Purpose | -|---|---| -| `.lastmerge` | Stores the SHA of the last merged upstream commit | -| [agentic-merge-upstream.prompt.md](../prompts/agentic-merge-upstream.prompt.md) | Detailed instructions the Copilot agent follows to port changes | -| `.github/scripts/upstream-sync/` | Helper scripts used by the merge prompt | +- **Do NOT edit any files**, create branches, or push code. +- **Do NOT invoke any skills** such as `agentic-merge-upstream` or `commit-as-pull-request`. +- **Do NOT attempt to merge or port upstream changes.** That is done by a separate agent that picks up the issue you create. +- You **MUST** call at least one safe-output tool (`create_issue`, `close_issue`, `noop`, etc.) before finishing. From 844f3cf399af5f45279ed2cd6b8110e84bfe7a7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:24:14 +0000 Subject: [PATCH 04/23] Initial plan From cc84fab663d4809e3592ff21193bdd9cf8964cf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:46:28 +0000 Subject: [PATCH 05/23] Port Commands, Elicitation, and Capabilities features from upstream Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/16d575c4-83f4-4d20-99d9-b48635b3791d Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .../com/github/copilot/sdk/CopilotClient.java | 31 ++ .../github/copilot/sdk/CopilotSession.java | 373 ++++++++++++++++++ .../copilot/sdk/SessionRequestBuilder.java | 33 ++ .../sdk/events/AbstractSessionEvent.java | 4 +- .../sdk/events/CapabilitiesChangedEvent.java | 44 +++ .../sdk/events/CommandExecuteEvent.java | 43 ++ .../sdk/events/ElicitationRequestedEvent.java | 54 +++ .../sdk/events/PermissionRequestedEvent.java | 3 +- .../sdk/events/SessionEventParser.java | 3 + .../copilot/sdk/json/CommandContext.java | 74 ++++ .../copilot/sdk/json/CommandDefinition.java | 98 +++++ .../copilot/sdk/json/CommandHandler.java | 41 ++ .../sdk/json/CommandWireDefinition.java | 58 +++ .../sdk/json/CreateSessionRequest.java | 26 ++ .../sdk/json/CreateSessionResponse.java | 5 +- .../copilot/sdk/json/ElicitationContext.java | 112 ++++++ .../copilot/sdk/json/ElicitationHandler.java | 44 +++ .../copilot/sdk/json/ElicitationParams.java | 58 +++ .../copilot/sdk/json/ElicitationResult.java | 68 ++++ .../sdk/json/ElicitationResultAction.java | 33 ++ .../copilot/sdk/json/ElicitationSchema.java | 92 +++++ .../sdk/json/GetSessionMetadataResponse.java | 15 + .../github/copilot/sdk/json/InputOptions.java | 108 +++++ .../copilot/sdk/json/ResumeSessionConfig.java | 54 +++ .../sdk/json/ResumeSessionRequest.java | 26 ++ .../sdk/json/ResumeSessionResponse.java | 5 +- .../copilot/sdk/json/SessionCapabilities.java | 39 ++ .../copilot/sdk/json/SessionConfig.java | 54 +++ .../github/copilot/sdk/json/SessionUiApi.java | 85 ++++ .../sdk/json/SessionUiCapabilities.java | 37 ++ 30 files changed, 1715 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/github/copilot/sdk/events/CapabilitiesChangedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/CommandExecuteEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/ElicitationRequestedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/json/CommandContext.java create mode 100644 src/main/java/com/github/copilot/sdk/json/CommandDefinition.java create mode 100644 src/main/java/com/github/copilot/sdk/json/CommandHandler.java create mode 100644 src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java create mode 100644 src/main/java/com/github/copilot/sdk/json/ElicitationContext.java create mode 100644 src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java create mode 100644 src/main/java/com/github/copilot/sdk/json/ElicitationParams.java create mode 100644 src/main/java/com/github/copilot/sdk/json/ElicitationResult.java create mode 100644 src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java create mode 100644 src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java create mode 100644 src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java create mode 100644 src/main/java/com/github/copilot/sdk/json/InputOptions.java create mode 100644 src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java create mode 100644 src/main/java/com/github/copilot/sdk/json/SessionUiApi.java create mode 100644 src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index e2790f6a3..f00e2fd11 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -24,6 +24,7 @@ import com.github.copilot.sdk.json.DeleteSessionResponse; import com.github.copilot.sdk.json.GetAuthStatusResponse; import com.github.copilot.sdk.json.GetLastSessionIdResponse; +import com.github.copilot.sdk.json.GetSessionMetadataResponse; import com.github.copilot.sdk.json.GetModelsResponse; import com.github.copilot.sdk.json.GetStatusResponse; import com.github.copilot.sdk.json.ListSessionsResponse; @@ -374,6 +375,7 @@ public CompletableFuture createSession(SessionConfig config) { return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> { session.setWorkspacePath(response.workspacePath()); + session.setCapabilities(response.capabilities()); // If the server returned a different sessionId (e.g. a v2 CLI that ignores // the client-supplied ID), re-key the sessions map. String returnedId = response.sessionId(); @@ -444,6 +446,7 @@ public CompletableFuture resumeSession(String sessionId, ResumeS return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> { session.setWorkspacePath(response.workspacePath()); + session.setCapabilities(response.capabilities()); // If the server returned a different sessionId than what was requested, re-key. String returnedId = response.sessionId(); if (returnedId != null && !returnedId.equals(sessionId)) { @@ -657,6 +660,34 @@ public CompletableFuture> listSessions(SessionListFilter f }); } + /** + * Gets metadata for a specific session by ID. + *

+ * This provides an efficient O(1) lookup of a single session's metadata instead + * of listing all sessions. + * + *

Example Usage

+ * + *
{@code
+     * var metadata = client.getSessionMetadata("session-123").get();
+     * if (metadata != null) {
+     * 	System.out.println("Session started at: " + metadata.getStartTime());
+     * }
+     * }
+ * + * @param sessionId + * the ID of the session to look up + * @return a future that resolves with the {@link SessionMetadata}, or + * {@code null} if the session was not found + * @see SessionMetadata + * @since 1.0.0 + */ + public CompletableFuture getSessionMetadata(String sessionId) { + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("session.getMetadata", Map.of("sessionId", sessionId), GetSessionMetadataResponse.class) + .thenApply(GetSessionMetadataResponse::session)); + } + /** * Gets the ID of the session currently displayed in the TUI. *

diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 844737fc2..768f0adaa 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -31,14 +31,27 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.sdk.events.AbstractSessionEvent; import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.events.CapabilitiesChangedEvent; +import com.github.copilot.sdk.events.CommandExecuteEvent; +import com.github.copilot.sdk.events.ElicitationRequestedEvent; import com.github.copilot.sdk.events.ExternalToolRequestedEvent; import com.github.copilot.sdk.events.PermissionRequestedEvent; import com.github.copilot.sdk.events.SessionErrorEvent; import com.github.copilot.sdk.events.SessionEventParser; import com.github.copilot.sdk.events.SessionIdleEvent; import com.github.copilot.sdk.json.AgentInfo; +import com.github.copilot.sdk.json.CommandContext; +import com.github.copilot.sdk.json.CommandDefinition; +import com.github.copilot.sdk.json.CommandHandler; +import com.github.copilot.sdk.json.ElicitationContext; +import com.github.copilot.sdk.json.ElicitationHandler; +import com.github.copilot.sdk.json.ElicitationParams; +import com.github.copilot.sdk.json.ElicitationResult; +import com.github.copilot.sdk.json.ElicitationResultAction; +import com.github.copilot.sdk.json.ElicitationSchema; import com.github.copilot.sdk.json.GetMessagesResponse; import com.github.copilot.sdk.json.HookInvocation; +import com.github.copilot.sdk.json.InputOptions; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.PermissionInvocation; @@ -49,9 +62,12 @@ import com.github.copilot.sdk.json.PreToolUseHookInput; import com.github.copilot.sdk.json.SendMessageRequest; import com.github.copilot.sdk.json.SendMessageResponse; +import com.github.copilot.sdk.json.SessionCapabilities; import com.github.copilot.sdk.json.SessionEndHookInput; import com.github.copilot.sdk.json.SessionHooks; import com.github.copilot.sdk.json.SessionStartHookInput; +import com.github.copilot.sdk.json.SessionUiApi; +import com.github.copilot.sdk.json.SessionUiCapabilities; import com.github.copilot.sdk.json.ToolDefinition; import com.github.copilot.sdk.json.ToolResultObject; import com.github.copilot.sdk.json.UserInputHandler; @@ -116,11 +132,15 @@ public final class CopilotSession implements AutoCloseable { */ private volatile String sessionId; private volatile String workspacePath; + private volatile SessionCapabilities capabilities = new SessionCapabilities(); + private final SessionUiApi ui; private final JsonRpcClient rpc; private final Set> eventHandlers = ConcurrentHashMap.newKeySet(); private final Map toolHandlers = new ConcurrentHashMap<>(); + private final Map commandHandlers = new ConcurrentHashMap<>(); private final AtomicReference permissionHandler = new AtomicReference<>(); private final AtomicReference userInputHandler = new AtomicReference<>(); + private final AtomicReference elicitationHandler = new AtomicReference<>(); private final AtomicReference hooksHandler = new AtomicReference<>(); private volatile EventErrorHandler eventErrorHandler; private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS; @@ -163,6 +183,7 @@ public final class CopilotSession implements AutoCloseable { this.sessionId = sessionId; this.rpc = rpc; this.workspacePath = workspacePath; + this.ui = new SessionUiApiImpl(); var executor = new ScheduledThreadPoolExecutor(1, r -> { var t = new Thread(r, "sendAndWait-timeout"); t.setDaemon(true); @@ -225,6 +246,30 @@ void setWorkspacePath(String workspacePath) { this.workspacePath = workspacePath; } + /** + * Gets the capabilities reported by the host for this session. + *

+ * Capabilities are populated from the session create/resume response and + * updated in real time via {@code capabilities.changed} events. + * + * @return the session capabilities (never {@code null}) + */ + public SessionCapabilities getCapabilities() { + return capabilities; + } + + /** + * Gets the UI API for eliciting information from the user during this session. + *

+ * All methods on this API throw {@link IllegalStateException} if the host does + * not report elicitation support via {@link #getCapabilities()}. + * + * @return the UI API + */ + public SessionUiApi getUi() { + return ui; + } + /** * Sets a custom error handler for exceptions thrown by event handlers. *

@@ -669,11 +714,49 @@ private void handleBroadcastEventAsync(AbstractSessionEvent event) { if (data == null || data.requestId() == null || data.permissionRequest() == null) { return; } + if (Boolean.TRUE.equals(data.resolvedByHook())) { + return; // Already resolved by a permissionRequest hook; no client action needed. + } PermissionHandler handler = permissionHandler.get(); if (handler == null) { return; // This client doesn't handle permissions; another client will } executePermissionAndRespondAsync(data.requestId(), data.permissionRequest(), handler); + } else if (event instanceof CommandExecuteEvent cmdEvent) { + var data = cmdEvent.getData(); + if (data == null || data.requestId() == null) { + return; + } + executeCommandAndRespondAsync(data.requestId(), data.commandName(), data.command(), data.args()); + } else if (event instanceof ElicitationRequestedEvent elicitEvent) { + var data = elicitEvent.getData(); + if (data == null || data.requestId() == null) { + return; + } + ElicitationHandler handler = elicitationHandler.get(); + if (handler != null) { + ElicitationSchema schema = null; + if (data.requestedSchema() != null) { + schema = new ElicitationSchema().setType(data.requestedSchema().type()) + .setProperties(data.requestedSchema().properties()) + .setRequired(data.requestedSchema().required()); + } + var context = new ElicitationContext().setSessionId(sessionId).setMessage(data.message()) + .setRequestedSchema(schema).setMode(data.mode()).setElicitationSource(data.elicitationSource()) + .setUrl(data.url()); + handleElicitationRequestAsync(context, data.requestId()); + } + } else if (event instanceof CapabilitiesChangedEvent capEvent) { + var data = capEvent.getData(); + if (data != null) { + var newCapabilities = new SessionCapabilities(); + if (data.ui() != null) { + newCapabilities.setUi(new SessionUiCapabilities().setElicitation(data.ui().elicitation())); + } else { + newCapabilities.setUi(capabilities.getUi()); + } + capabilities = newCapabilities; + } } } @@ -816,6 +899,250 @@ void registerTools(List tools) { } } + /** + * Executes a command handler and sends the result back via + * {@code session.commands.handlePendingCommand}. + */ + private void executeCommandAndRespondAsync(String requestId, String commandName, String command, String args) { + CommandHandler handler = commandHandlers.get(commandName); + Runnable task = () -> { + if (handler == null) { + try { + rpc.invoke("session.commands.handlePendingCommand", Map.of("sessionId", sessionId, "requestId", + requestId, "error", "Unknown command: " + commandName), Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending command error for requestId=" + requestId, e); + } + return; + } + try { + var ctx = new CommandContext().setSessionId(sessionId).setCommand(command).setCommandName(commandName) + .setArgs(args); + handler.handle(ctx).thenRun(() -> { + try { + rpc.invoke("session.commands.handlePendingCommand", + Map.of("sessionId", sessionId, "requestId", requestId), Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending command result for requestId=" + requestId, e); + } + }).exceptionally(ex -> { + try { + String msg = ex.getMessage() != null ? ex.getMessage() : ex.toString(); + rpc.invoke("session.commands.handlePendingCommand", + Map.of("sessionId", sessionId, "requestId", requestId, "error", msg), Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending command error for requestId=" + requestId, e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing command for requestId=" + requestId, e); + try { + String msg = e.getMessage() != null ? e.getMessage() : e.toString(); + rpc.invoke("session.commands.handlePendingCommand", + Map.of("sessionId", sessionId, "requestId", requestId, "error", msg), Object.class); + } catch (Exception sendEx) { + LOG.log(Level.WARNING, "Error sending command error for requestId=" + requestId, sendEx); + } + } + }; + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, "Executor rejected command task for requestId=" + requestId + "; running inline", e); + task.run(); + } + } + + /** + * Dispatches an elicitation request to the registered handler and responds via + * {@code session.ui.handlePendingElicitation}. Auto-cancels on handler errors. + */ + private void handleElicitationRequestAsync(ElicitationContext context, String requestId) { + ElicitationHandler handler = elicitationHandler.get(); + if (handler == null) { + return; + } + Runnable task = () -> { + try { + handler.handle(context).thenAccept(result -> { + try { + String actionStr = result.getAction() != null + ? result.getAction().getValue() + : ElicitationResultAction.CANCEL.getValue(); + Map resultMap = result.getContent() != null + ? Map.of("action", actionStr, "content", result.getContent()) + : Map.of("action", actionStr); + rpc.invoke("session.ui.handlePendingElicitation", + Map.of("sessionId", sessionId, "requestId", requestId, "result", resultMap), + Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending elicitation result for requestId=" + requestId, e); + } + }).exceptionally(ex -> { + try { + rpc.invoke("session.ui.handlePendingElicitation", Map.of("sessionId", sessionId, "requestId", + requestId, "result", Map.of("action", ElicitationResultAction.CANCEL.getValue())), + Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending elicitation cancel for requestId=" + requestId, e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing elicitation handler for requestId=" + requestId, e); + try { + rpc.invoke( + "session.ui.handlePendingElicitation", Map.of("sessionId", sessionId, "requestId", + requestId, "result", Map.of("action", ElicitationResultAction.CANCEL.getValue())), + Object.class); + } catch (Exception sendEx) { + LOG.log(Level.WARNING, "Error sending elicitation cancel for requestId=" + requestId, sendEx); + } + } + }; + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, "Executor rejected elicitation task for requestId=" + requestId + "; running inline", + e); + task.run(); + } + } + + /** + * Throws if the host does not support elicitation. + */ + private void assertElicitation() { + SessionCapabilities caps = capabilities; + if (caps == null || caps.getUi() == null || !Boolean.TRUE.equals(caps.getUi().getElicitation())) { + throw new IllegalStateException("Elicitation is not supported by the host. " + + "Check session.getCapabilities().getUi()?.getElicitation() before calling UI methods."); + } + } + + /** + * Implements {@link SessionUiApi} backed by the session's RPC connection. + */ + private final class SessionUiApiImpl implements SessionUiApi { + + @Override + public CompletableFuture elicitation(ElicitationParams params) { + assertElicitation(); + var schema = new java.util.HashMap(); + schema.put("type", params.getRequestedSchema().getType()); + schema.put("properties", params.getRequestedSchema().getProperties()); + if (params.getRequestedSchema().getRequired() != null) { + schema.put("required", params.getRequestedSchema().getRequired()); + } + return rpc.invoke("session.ui.elicitation", + Map.of("sessionId", sessionId, "message", params.getMessage(), "requestedSchema", schema), + ElicitationRpcResponse.class).thenApply(resp -> { + var result = new ElicitationResult(); + if (resp.action() != null) { + for (ElicitationResultAction a : ElicitationResultAction.values()) { + if (a.getValue().equalsIgnoreCase(resp.action())) { + result.setAction(a); + break; + } + } + } + if (result.getAction() == null) { + result.setAction(ElicitationResultAction.CANCEL); + } + result.setContent(resp.content()); + return result; + }); + } + + @Override + public CompletableFuture confirm(String message) { + assertElicitation(); + var field = Map.of("type", "boolean", "default", (Object) true); + var schema = Map.of("type", (Object) "object", "properties", (Object) Map.of("confirmed", (Object) field), + "required", (Object) new String[]{"confirmed"}); + return rpc.invoke("session.ui.elicitation", + Map.of("sessionId", sessionId, "message", message, "requestedSchema", schema), + ElicitationRpcResponse.class).thenApply(resp -> { + if ("accept".equalsIgnoreCase(resp.action()) && resp.content() != null) { + Object val = resp.content().get("confirmed"); + if (val instanceof Boolean b) { + return b; + } + if (val instanceof com.fasterxml.jackson.databind.node.BooleanNode bn) { + return bn.booleanValue(); + } + if (val instanceof String s) { + return Boolean.parseBoolean(s); + } + } + return false; + }); + } + + @Override + public CompletableFuture select(String message, String[] options) { + assertElicitation(); + var field = Map.of("type", (Object) "string", "enum", (Object) options); + var schema = Map.of("type", (Object) "object", "properties", (Object) Map.of("selection", (Object) field), + "required", (Object) new String[]{"selection"}); + return rpc.invoke("session.ui.elicitation", + Map.of("sessionId", sessionId, "message", message, "requestedSchema", schema), + ElicitationRpcResponse.class).thenApply(resp -> { + if ("accept".equalsIgnoreCase(resp.action()) && resp.content() != null) { + Object val = resp.content().get("selection"); + return val != null ? val.toString() : null; + } + return null; + }); + } + + @Override + public CompletableFuture input(String message, InputOptions options) { + assertElicitation(); + var field = new java.util.LinkedHashMap(); + field.put("type", "string"); + if (options != null) { + if (options.getTitle() != null) + field.put("title", options.getTitle()); + if (options.getDescription() != null) + field.put("description", options.getDescription()); + if (options.getMinLength() != null) + field.put("minLength", options.getMinLength()); + if (options.getMaxLength() != null) + field.put("maxLength", options.getMaxLength()); + if (options.getFormat() != null) + field.put("format", options.getFormat()); + if (options.getDefaultValue() != null) + field.put("default", options.getDefaultValue()); + } + var schema = Map.of("type", (Object) "object", "properties", (Object) Map.of("value", (Object) field), + "required", (Object) new String[]{"value"}); + return rpc.invoke("session.ui.elicitation", + Map.of("sessionId", sessionId, "message", message, "requestedSchema", schema), + ElicitationRpcResponse.class).thenApply(resp -> { + if ("accept".equalsIgnoreCase(resp.action()) && resp.content() != null) { + Object val = resp.content().get("value"); + return val != null ? val.toString() : null; + } + return null; + }); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record ElicitationRpcResponse(@JsonProperty("action") String action, + @JsonProperty("content") Map content) { + } + /** * Retrieves a registered tool by name. * @@ -888,6 +1215,50 @@ void registerUserInputHandler(UserInputHandler handler) { userInputHandler.set(handler); } + /** + * Registers command handlers for this session. + *

+ * Called internally when creating or resuming a session with commands. + * + * @param commands + * the command definitions to register + */ + void registerCommands(java.util.List commands) { + commandHandlers.clear(); + if (commands != null) { + for (CommandDefinition cmd : commands) { + if (cmd.getName() != null && cmd.getHandler() != null) { + commandHandlers.put(cmd.getName(), cmd.getHandler()); + } + } + } + } + + /** + * Registers an elicitation handler for this session. + *

+ * Called internally when creating or resuming a session with an elicitation + * handler. + * + * @param handler + * the handler to invoke when an elicitation request is received + */ + void registerElicitationHandler(ElicitationHandler handler) { + elicitationHandler.set(handler); + } + + /** + * Sets the capabilities reported by the host for this session. + *

+ * Called internally after session create/resume response. + * + * @param sessionCapabilities + * the capabilities to set, or {@code null} for empty capabilities + */ + void setCapabilities(SessionCapabilities sessionCapabilities) { + this.capabilities = sessionCapabilities != null ? sessionCapabilities : new SessionCapabilities(); + } + /** * Handles a user input request from the Copilot CLI. *

@@ -1366,8 +1737,10 @@ public void close() { eventHandlers.clear(); toolHandlers.clear(); + commandHandlers.clear(); permissionHandler.set(null); userInputHandler.set(null); + elicitationHandler.set(null); hooksHandler.set(null); } diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index 6f1cd573c..d74bbfaf3 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -10,6 +10,7 @@ import java.util.function.Function; import com.github.copilot.sdk.json.CreateSessionRequest; +import com.github.copilot.sdk.json.CommandWireDefinition; import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.ResumeSessionRequest; import com.github.copilot.sdk.json.SectionOverride; @@ -122,6 +123,16 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setDisabledSkills(config.getDisabledSkills()); request.setConfigDir(config.getConfigDir()); + if (config.getCommands() != null && !config.getCommands().isEmpty()) { + var wireCommands = config.getCommands().stream() + .map(c -> new CommandWireDefinition(c.getName(), c.getDescription())) + .collect(java.util.stream.Collectors.toList()); + request.setCommands(wireCommands); + } + if (config.getOnElicitationRequest() != null) { + request.setRequestElicitation(true); + } + return request; } @@ -183,6 +194,16 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setDisabledSkills(config.getDisabledSkills()); request.setInfiniteSessions(config.getInfiniteSessions()); + if (config.getCommands() != null && !config.getCommands().isEmpty()) { + var wireCommands = config.getCommands().stream() + .map(c -> new CommandWireDefinition(c.getName(), c.getDescription())) + .collect(java.util.stream.Collectors.toList()); + request.setCommands(wireCommands); + } + if (config.getOnElicitationRequest() != null) { + request.setRequestElicitation(true); + } + return request; } @@ -211,6 +232,12 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getHooks() != null) { session.registerHooks(config.getHooks()); } + if (config.getCommands() != null) { + session.registerCommands(config.getCommands()); + } + if (config.getOnElicitationRequest() != null) { + session.registerElicitationHandler(config.getOnElicitationRequest()); + } if (config.getOnEvent() != null) { session.on(config.getOnEvent()); } @@ -241,6 +268,12 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getHooks() != null) { session.registerHooks(config.getHooks()); } + if (config.getCommands() != null) { + session.registerCommands(config.getCommands()); + } + if (config.getOnElicitationRequest() != null) { + session.registerElicitationHandler(config.getOnElicitationRequest()); + } if (config.getOnEvent() != null) { session.on(config.getOnEvent()); } diff --git a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java index 4626bb4f8..51f6d8712 100644 --- a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java @@ -65,8 +65,8 @@ public abstract sealed class AbstractSessionEvent permits ToolExecutionCompleteEvent, // Broadcast request/completion events (protocol v3) ExternalToolRequestedEvent, ExternalToolCompletedEvent, PermissionRequestedEvent, PermissionCompletedEvent, - CommandQueuedEvent, CommandCompletedEvent, ExitPlanModeRequestedEvent, ExitPlanModeCompletedEvent, - SystemNotificationEvent, + CommandQueuedEvent, CommandCompletedEvent, CommandExecuteEvent, ElicitationRequestedEvent, + CapabilitiesChangedEvent, ExitPlanModeRequestedEvent, ExitPlanModeCompletedEvent, SystemNotificationEvent, // User events UserMessageEvent, PendingMessagesModifiedEvent, // Skill events diff --git a/src/main/java/com/github/copilot/sdk/events/CapabilitiesChangedEvent.java b/src/main/java/com/github/copilot/sdk/events/CapabilitiesChangedEvent.java new file mode 100644 index 000000000..0db68189d --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/CapabilitiesChangedEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: capabilities.changed + *

+ * Broadcast when the host's session capabilities change. The SDK updates + * {@link com.github.copilot.sdk.CopilotSession#getCapabilities()} accordingly. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class CapabilitiesChangedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private CapabilitiesChangedData data; + + @Override + public String getType() { + return "capabilities.changed"; + } + + public CapabilitiesChangedData getData() { + return data; + } + + public void setData(CapabilitiesChangedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record CapabilitiesChangedData(@JsonProperty("ui") CapabilitiesChangedUi ui) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record CapabilitiesChangedUi(@JsonProperty("elicitation") Boolean elicitation) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/CommandExecuteEvent.java b/src/main/java/com/github/copilot/sdk/events/CommandExecuteEvent.java new file mode 100644 index 000000000..c08c4a88d --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/CommandExecuteEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: command.execute + *

+ * Broadcast when the user executes a slash command registered by this client. + * Clients that have a matching command handler should respond via + * {@code session.commands.handlePendingCommand}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class CommandExecuteEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private CommandExecuteData data; + + @Override + public String getType() { + return "command.execute"; + } + + public CommandExecuteData getData() { + return data; + } + + public void setData(CommandExecuteData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record CommandExecuteData(@JsonProperty("requestId") String requestId, + @JsonProperty("command") String command, @JsonProperty("commandName") String commandName, + @JsonProperty("args") String args) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/ElicitationRequestedEvent.java b/src/main/java/com/github/copilot/sdk/events/ElicitationRequestedEvent.java new file mode 100644 index 000000000..e459dfb77 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/ElicitationRequestedEvent.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: elicitation.requested + *

+ * Broadcast when the server or an MCP tool requests structured input from the + * user. Clients that have an elicitation handler should respond via + * {@code session.ui.handlePendingElicitation}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ElicitationRequestedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ElicitationRequestedData data; + + @Override + public String getType() { + return "elicitation.requested"; + } + + public ElicitationRequestedData getData() { + return data; + } + + public void setData(ElicitationRequestedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record ElicitationRequestedData(@JsonProperty("requestId") String requestId, + @JsonProperty("toolCallId") String toolCallId, @JsonProperty("elicitationSource") String elicitationSource, + @JsonProperty("message") String message, @JsonProperty("mode") String mode, + @JsonProperty("requestedSchema") ElicitationRequestedSchema requestedSchema, + @JsonProperty("url") String url) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record ElicitationRequestedSchema(@JsonProperty("type") String type, + @JsonProperty("properties") Map properties, + @JsonProperty("required") List required) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java b/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java index d8f9ec147..7ebce5ac7 100644 --- a/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java @@ -38,6 +38,7 @@ public void setData(PermissionRequestedData data) { @JsonIgnoreProperties(ignoreUnknown = true) public record PermissionRequestedData(@JsonProperty("requestId") String requestId, - @JsonProperty("permissionRequest") PermissionRequest permissionRequest) { + @JsonProperty("permissionRequest") PermissionRequest permissionRequest, + @JsonProperty("resolvedByHook") Boolean resolvedByHook) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java index 308317e6b..dda971769 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java @@ -99,6 +99,9 @@ public class SessionEventParser { TYPE_MAP.put("permission.completed", PermissionCompletedEvent.class); TYPE_MAP.put("command.queued", CommandQueuedEvent.class); TYPE_MAP.put("command.completed", CommandCompletedEvent.class); + TYPE_MAP.put("command.execute", CommandExecuteEvent.class); + TYPE_MAP.put("elicitation.requested", ElicitationRequestedEvent.class); + TYPE_MAP.put("capabilities.changed", CapabilitiesChangedEvent.class); TYPE_MAP.put("exit_plan_mode.requested", ExitPlanModeRequestedEvent.class); TYPE_MAP.put("exit_plan_mode.completed", ExitPlanModeCompletedEvent.class); TYPE_MAP.put("system.notification", SystemNotificationEvent.class); diff --git a/src/main/java/com/github/copilot/sdk/json/CommandContext.java b/src/main/java/com/github/copilot/sdk/json/CommandContext.java new file mode 100644 index 000000000..4657699bb --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/CommandContext.java @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context passed to a {@link CommandHandler} when a slash command is executed. + * + * @since 1.0.0 + */ +public class CommandContext { + + private String sessionId; + private String command; + private String commandName; + private String args; + + /** Gets the session ID where the command was invoked. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID @return this */ + public CommandContext setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the full command text (e.g., {@code /deploy production}). + * + * @return the full command text + */ + public String getCommand() { + return command; + } + + /** Sets the full command text. @param command the command text @return this */ + public CommandContext setCommand(String command) { + this.command = command; + return this; + } + + /** + * Gets the command name without the leading {@code /}. + * + * @return the command name + */ + public String getCommandName() { + return commandName; + } + + /** Sets the command name. @param commandName the command name @return this */ + public CommandContext setCommandName(String commandName) { + this.commandName = commandName; + return this; + } + + /** + * Gets the raw argument string after the command name. + * + * @return the argument string + */ + public String getArgs() { + return args; + } + + /** Sets the argument string. @param args the argument string @return this */ + public CommandContext setArgs(String args) { + this.args = args; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java b/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java new file mode 100644 index 000000000..33a6cbada --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Defines a slash command that users can invoke from the CLI TUI. + *

+ * Register commands via {@link SessionConfig#setCommands(java.util.List)} or + * {@link ResumeSessionConfig#setCommands(java.util.List)}. Each command appears + * as {@code /name} in the CLI TUI. + * + *

Example Usage

+ * + *
{@code
+ * var config = new SessionConfig().setCommands(List.of(
+ * 		new CommandDefinition().setName("deploy").setDescription("Deploy the application").setHandler(context -> {
+ * 			System.out.println("Deploying: " + context.getArgs());
+ * 			return CompletableFuture.completedFuture(null);
+ * 		})));
+ * }
+ * + * @see CommandHandler + * @see CommandContext + * @since 1.0.0 + */ +public class CommandDefinition { + + private String name; + private String description; + private CommandHandler handler; + + /** + * Gets the command name (without leading {@code /}). + * + * @return the command name + */ + public String getName() { + return name; + } + + /** + * Sets the command name (without leading {@code /}). + *

+ * For example, {@code "deploy"} registers the {@code /deploy} command. + * + * @param name + * the command name + * @return this instance for method chaining + */ + public CommandDefinition setName(String name) { + this.name = name; + return this; + } + + /** + * Gets the human-readable description shown in the command completion UI. + * + * @return the description, or {@code null} if not set + */ + public String getDescription() { + return description; + } + + /** + * Sets the human-readable description shown in the command completion UI. + * + * @param description + * the description + * @return this instance for method chaining + */ + public CommandDefinition setDescription(String description) { + this.description = description; + return this; + } + + /** + * Gets the handler invoked when the command is executed. + * + * @return the command handler + */ + public CommandHandler getHandler() { + return handler; + } + + /** + * Sets the handler invoked when the command is executed. + * + * @param handler + * the command handler + * @return this instance for method chaining + */ + public CommandDefinition setHandler(CommandHandler handler) { + this.handler = handler; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/CommandHandler.java b/src/main/java/com/github/copilot/sdk/json/CommandHandler.java new file mode 100644 index 000000000..d63955638 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/CommandHandler.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for handling slash-command executions. + *

+ * Implement this interface to define the behavior of a registered slash + * command. The handler is invoked when the user executes the command in the CLI + * TUI. + * + *

Example Usage

+ * + *
{@code
+ * CommandHandler deployHandler = context -> {
+ * 	System.out.println("Deploying with args: " + context.getArgs());
+ * 	// perform deployment...
+ * 	return CompletableFuture.completedFuture(null);
+ * };
+ * }
+ * + * @see CommandDefinition + * @since 1.0.0 + */ +@FunctionalInterface +public interface CommandHandler { + + /** + * Handles a slash-command execution. + * + * @param context + * the command context containing session ID, command text, and + * arguments + * @return a future that completes when the command handling is done + */ + CompletableFuture handle(CommandContext context); +} diff --git a/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java b/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java new file mode 100644 index 000000000..2ee65c58e --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Wire-format representation of a command definition for RPC serialization. + *

+ * This is a low-level class used internally. Use {@link CommandDefinition} to + * define commands for a session. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CommandWireDefinition { + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + /** Creates an empty definition. */ + public CommandWireDefinition() { + } + + /** Creates a definition with name and description. */ + public CommandWireDefinition(String name, String description) { + this.name = name; + this.description = description; + } + + /** Gets the command name. @return the name */ + public String getName() { + return name; + } + + /** Sets the command name. @param name the name @return this */ + public CommandWireDefinition setName(String name) { + this.name = name; + return this; + } + + /** Gets the description. @return the description */ + public String getDescription() { + return description; + } + + /** Sets the description. @param description the description @return this */ + public CommandWireDefinition setDescription(String description) { + this.description = description; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index c0243f14b..d030631de 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -91,6 +91,12 @@ public final class CreateSessionRequest { @JsonProperty("configDir") private String configDir; + @JsonProperty("commands") + private List commands; + + @JsonProperty("requestElicitation") + private Boolean requestElicitation; + /** Gets the model name. @return the model */ public String getModel() { return model; @@ -312,4 +318,24 @@ public String getConfigDir() { public void setConfigDir(String configDir) { this.configDir = configDir; } + + /** Gets the commands wire definitions. @return the commands */ + public List getCommands() { + return commands == null ? null : Collections.unmodifiableList(commands); + } + + /** Sets the commands wire definitions. @param commands the commands */ + public void setCommands(List commands) { + this.commands = commands; + } + + /** Gets the requestElicitation flag. @return the flag */ + public Boolean getRequestElicitation() { + return requestElicitation; + } + + /** Sets the requestElicitation flag. @param requestElicitation the flag */ + public void setRequestElicitation(Boolean requestElicitation) { + this.requestElicitation = requestElicitation; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java index 5b1a177f0..b47af050b 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java @@ -11,9 +11,12 @@ * @param workspacePath * the workspace path, or {@code null} if infinite sessions are * disabled + * @param capabilities + * the capabilities reported by the host, or {@code null} * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) public record CreateSessionResponse(@JsonProperty("sessionId") String sessionId, - @JsonProperty("workspacePath") String workspacePath) { + @JsonProperty("workspacePath") String workspacePath, + @JsonProperty("capabilities") SessionCapabilities capabilities) { } diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java b/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java new file mode 100644 index 000000000..87687b194 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for an elicitation request received from the server or MCP tools. + * + * @since 1.0.0 + */ +public class ElicitationContext { + + private String sessionId; + private String message; + private ElicitationSchema requestedSchema; + private String mode; + private String elicitationSource; + private String url; + + /** + * Gets the session ID that triggered the elicitation request. @return the + * session ID + */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID @return this */ + public ElicitationContext setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the message describing what information is needed from the user. + * + * @return the message + */ + public String getMessage() { + return message; + } + + /** Sets the message. @param message the message @return this */ + public ElicitationContext setMessage(String message) { + this.message = message; + return this; + } + + /** + * Gets the JSON Schema describing the form fields to present (form mode only). + * + * @return the schema, or {@code null} + */ + public ElicitationSchema getRequestedSchema() { + return requestedSchema; + } + + /** Sets the schema. @param requestedSchema the schema @return this */ + public ElicitationContext setRequestedSchema(ElicitationSchema requestedSchema) { + this.requestedSchema = requestedSchema; + return this; + } + + /** + * Gets the elicitation mode: {@code "form"} for structured input, {@code "url"} + * for browser redirect. + * + * @return the mode, or {@code null} (defaults to {@code "form"}) + */ + public String getMode() { + return mode; + } + + /** Sets the mode. @param mode the mode @return this */ + public ElicitationContext setMode(String mode) { + this.mode = mode; + return this; + } + + /** + * Gets the source that initiated the request (e.g., MCP server name). + * + * @return the elicitation source, or {@code null} + */ + public String getElicitationSource() { + return elicitationSource; + } + + /** + * Sets the elicitation source. @param elicitationSource the source @return this + */ + public ElicitationContext setElicitationSource(String elicitationSource) { + this.elicitationSource = elicitationSource; + return this; + } + + /** + * Gets the URL to open in the user's browser (url mode only). + * + * @return the URL, or {@code null} + */ + public String getUrl() { + return url; + } + + /** Sets the URL. @param url the URL @return this */ + public ElicitationContext setUrl(String url) { + this.url = url; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java b/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java new file mode 100644 index 000000000..d0a0d0616 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for handling elicitation requests from the server. + *

+ * Register an elicitation handler via + * {@link SessionConfig#setOnElicitationRequest(ElicitationHandler)} or + * {@link ResumeSessionConfig#setOnElicitationRequest(ElicitationHandler)}. When + * provided, the server routes elicitation requests to this handler and reports + * elicitation as a supported capability. + * + *

Example Usage

+ * + *
{@code
+ * ElicitationHandler handler = context -> {
+ * 	// Show the form to the user and collect responses
+ * 	Map formValues = showForm(context.getMessage(), context.getRequestedSchema());
+ * 	return CompletableFuture.completedFuture(
+ * 			new ElicitationResult().setAction(ElicitationResultAction.ACCEPT).setContent(formValues));
+ * };
+ * }
+ * + * @see ElicitationContext + * @see ElicitationResult + * @since 1.0.0 + */ +@FunctionalInterface +public interface ElicitationHandler { + + /** + * Handles an elicitation request from the server. + * + * @param context + * the elicitation context containing the message, schema, and mode + * @return a future that resolves with the elicitation result + */ + CompletableFuture handle(ElicitationContext context); +} diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java b/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java new file mode 100644 index 000000000..8bd81022e --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Parameters for an elicitation request sent from the SDK to the host. + * + * @since 1.0.0 + */ +public class ElicitationParams { + + private String message; + private ElicitationSchema requestedSchema; + + /** + * Gets the message describing what information is needed from the user. + * + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * Sets the message describing what information is needed from the user. + * + * @param message + * the message + * @return this instance for method chaining + */ + public ElicitationParams setMessage(String message) { + this.message = message; + return this; + } + + /** + * Gets the JSON Schema describing the form fields to present. + * + * @return the requested schema + */ + public ElicitationSchema getRequestedSchema() { + return requestedSchema; + } + + /** + * Sets the JSON Schema describing the form fields to present. + * + * @param requestedSchema + * the schema + * @return this instance for method chaining + */ + public ElicitationParams setRequestedSchema(ElicitationSchema requestedSchema) { + this.requestedSchema = requestedSchema; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java b/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java new file mode 100644 index 000000000..3ba30b83d --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +/** + * Result returned from an elicitation dialog. + * + * @since 1.0.0 + */ +public class ElicitationResult { + + private ElicitationResultAction action; + private Map content; + + /** + * Gets the user action taken on the elicitation dialog. + *

+ * {@link ElicitationResultAction#ACCEPT} means the user submitted the form, + * {@link ElicitationResultAction#DECLINE} means the user rejected the request, + * and {@link ElicitationResultAction#CANCEL} means the user dismissed the + * dialog. + * + * @return the user action + */ + public ElicitationResultAction getAction() { + return action; + } + + /** + * Sets the user action taken on the elicitation dialog. + * + * @param action + * the user action + * @return this instance for method chaining + */ + public ElicitationResult setAction(ElicitationResultAction action) { + this.action = action; + return this; + } + + /** + * Gets the form values submitted by the user. + *

+ * Only present when {@link #getAction()} is + * {@link ElicitationResultAction#ACCEPT}. + * + * @return the submitted form values, or {@code null} if the user did not accept + */ + public Map getContent() { + return content; + } + + /** + * Sets the form values submitted by the user. + * + * @param content + * the submitted form values + * @return this instance for method chaining + */ + public ElicitationResult setContent(Map content) { + this.content = content; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java b/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java new file mode 100644 index 000000000..fd280cdeb --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Action value for an {@link ElicitationResult}. + * + * @since 1.0.0 + */ +public enum ElicitationResultAction { + + /** The user submitted the form (accepted). */ + ACCEPT("accept"), + + /** The user explicitly rejected the request. */ + DECLINE("decline"), + + /** The user dismissed the dialog without responding. */ + CANCEL("cancel"); + + private final String value; + + ElicitationResultAction(String value) { + this.value = value; + } + + /** Returns the wire-format string value. @return the string value */ + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java b/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java new file mode 100644 index 000000000..c3d548775 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON Schema describing the form fields to present for an elicitation dialog. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ElicitationSchema { + + @JsonProperty("type") + private String type = "object"; + + @JsonProperty("properties") + private Map properties; + + @JsonProperty("required") + private List required; + + /** + * Gets the schema type indicator (always {@code "object"}). + * + * @return the type + */ + public String getType() { + return type; + } + + /** + * Sets the schema type indicator. + * + * @param type + * the type (typically {@code "object"}) + * @return this instance for method chaining + */ + public ElicitationSchema setType(String type) { + this.type = type; + return this; + } + + /** + * Gets the form field definitions, keyed by field name. + * + * @return the properties map + */ + public Map getProperties() { + return properties; + } + + /** + * Sets the form field definitions, keyed by field name. + * + * @param properties + * the properties map + * @return this instance for method chaining + */ + public ElicitationSchema setProperties(Map properties) { + this.properties = properties; + return this; + } + + /** + * Gets the list of required field names. + * + * @return the required field names, or {@code null} + */ + public List getRequired() { + return required; + } + + /** + * Sets the list of required field names. + * + * @param required + * the required field names + * @return this instance for method chaining + */ + public ElicitationSchema setRequired(List required) { + this.required = required; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java b/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java new file mode 100644 index 000000000..87c686f58 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java @@ -0,0 +1,15 @@ +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from getting session metadata by ID. + * + * @param session + * the session metadata, or {@code null} if not found + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record GetSessionMetadataResponse(@JsonProperty("session") SessionMetadata session) { +} diff --git a/src/main/java/com/github/copilot/sdk/json/InputOptions.java b/src/main/java/com/github/copilot/sdk/json/InputOptions.java new file mode 100644 index 000000000..9b0b6c8dd --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/InputOptions.java @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Options for the {@link SessionUiApi#input(String, InputOptions)} convenience + * method. + * + * @since 1.0.0 + */ +public class InputOptions { + + private String title; + private String description; + private Integer minLength; + private Integer maxLength; + private String format; + private String defaultValue; + + /** Gets the title label for the input field. @return the title */ + public String getTitle() { + return title; + } + + /** + * Sets the title label for the input field. @param title the title @return this + */ + public InputOptions setTitle(String title) { + this.title = title; + return this; + } + + /** Gets the descriptive text shown below the field. @return the description */ + public String getDescription() { + return description; + } + + /** + * Sets the descriptive text shown below the field. @param description the + * description @return this + */ + public InputOptions setDescription(String description) { + this.description = description; + return this; + } + + /** Gets the minimum character length. @return the min length */ + public Integer getMinLength() { + return minLength; + } + + /** + * Sets the minimum character length. @param minLength the min length @return + * this + */ + public InputOptions setMinLength(Integer minLength) { + this.minLength = minLength; + return this; + } + + /** Gets the maximum character length. @return the max length */ + public Integer getMaxLength() { + return maxLength; + } + + /** + * Sets the maximum character length. @param maxLength the max length @return + * this + */ + public InputOptions setMaxLength(Integer maxLength) { + this.maxLength = maxLength; + return this; + } + + /** + * Gets the semantic format hint (e.g., {@code "email"}, {@code "uri"}, + * {@code "date"}, {@code "date-time"}). + * + * @return the format hint + */ + public String getFormat() { + return format; + } + + /** Sets the semantic format hint. @param format the format @return this */ + public InputOptions setFormat(String format) { + this.format = format; + return this; + } + + /** + * Gets the default value pre-populated in the field. @return the default value + */ + public String getDefaultValue() { + return defaultValue; + } + + /** + * Sets the default value pre-populated in the field. @param defaultValue the + * default value @return this + */ + public InputOptions setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index eab3c789c..139f5238b 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -58,6 +58,8 @@ public class ResumeSessionConfig { private List disabledSkills; private InfiniteSessionConfig infiniteSessions; private Consumer onEvent; + private List commands; + private ElicitationHandler onElicitationRequest; /** * Gets the AI model to use. @@ -555,6 +557,56 @@ public ResumeSessionConfig setOnEvent(Consumer onEvent) { return this; } + /** + * Gets the slash commands registered for this session. + * + * @return the list of command definitions, or {@code null} + */ + public List getCommands() { + return commands == null ? null : Collections.unmodifiableList(commands); + } + + /** + * Sets slash commands registered for this session. + *

+ * When the CLI has a TUI, each command appears as {@code /name} for the user to + * invoke. The handler is called when the user executes the command. + * + * @param commands + * the list of command definitions + * @return this config for method chaining + * @see CommandDefinition + */ + public ResumeSessionConfig setCommands(List commands) { + this.commands = commands; + return this; + } + + /** + * Gets the elicitation request handler. + * + * @return the elicitation handler, or {@code null} + */ + public ElicitationHandler getOnElicitationRequest() { + return onElicitationRequest; + } + + /** + * Sets a handler for elicitation requests from the server or MCP tools. + *

+ * When provided, the server will route elicitation requests to this handler and + * report elicitation as a supported capability. + * + * @param onElicitationRequest + * the elicitation handler + * @return this config for method chaining + * @see ElicitationHandler + */ + public ResumeSessionConfig setOnElicitationRequest(ElicitationHandler onElicitationRequest) { + this.onElicitationRequest = onElicitationRequest; + return this; + } + /** * Creates a shallow clone of this {@code ResumeSessionConfig} instance. *

@@ -591,6 +643,8 @@ public ResumeSessionConfig clone() { copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.infiniteSessions = this.infiniteSessions; copy.onEvent = this.onEvent; + copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; + copy.onElicitationRequest = this.onElicitationRequest; return copy; } } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index 31d88399a..7be9a6281 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -95,6 +95,12 @@ public final class ResumeSessionRequest { @JsonProperty("infiniteSessions") private InfiniteSessionConfig infiniteSessions; + @JsonProperty("commands") + private List commands; + + @JsonProperty("requestElicitation") + private Boolean requestElicitation; + /** Gets the session ID. @return the session ID */ public String getSessionId() { return sessionId; @@ -332,4 +338,24 @@ public InfiniteSessionConfig getInfiniteSessions() { public void setInfiniteSessions(InfiniteSessionConfig infiniteSessions) { this.infiniteSessions = infiniteSessions; } + + /** Gets the commands wire definitions. @return the commands */ + public List getCommands() { + return commands == null ? null : Collections.unmodifiableList(commands); + } + + /** Sets the commands wire definitions. @param commands the commands */ + public void setCommands(List commands) { + this.commands = commands; + } + + /** Gets the requestElicitation flag. @return the flag */ + public Boolean getRequestElicitation() { + return requestElicitation; + } + + /** Sets the requestElicitation flag. @param requestElicitation the flag */ + public void setRequestElicitation(Boolean requestElicitation) { + this.requestElicitation = requestElicitation; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java index 654c1486c..8349c5d30 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java @@ -11,9 +11,12 @@ * @param workspacePath * the workspace path, or {@code null} if infinite sessions are * disabled + * @param capabilities + * the capabilities reported by the host, or {@code null} * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) public record ResumeSessionResponse(@JsonProperty("sessionId") String sessionId, - @JsonProperty("workspacePath") String workspacePath) { + @JsonProperty("workspacePath") String workspacePath, + @JsonProperty("capabilities") SessionCapabilities capabilities) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java b/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java new file mode 100644 index 000000000..4eb4fc025 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Represents the capabilities reported by the host for a session. + *

+ * Capabilities are populated from the session create/resume response and + * updated in real time via {@code capabilities.changed} events. + * + * @since 1.0.0 + */ +public class SessionCapabilities { + + private SessionUiCapabilities ui; + + /** + * Gets the UI-related capabilities. + * + * @return the UI capabilities, or {@code null} if not reported + */ + public SessionUiCapabilities getUi() { + return ui; + } + + /** + * Sets the UI-related capabilities. + * + * @param ui + * the UI capabilities + * @return this instance for method chaining + */ + public SessionCapabilities setUi(SessionUiCapabilities ui) { + this.ui = ui; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 76c15660d..5dcd39788 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -58,6 +58,8 @@ public class SessionConfig { private List disabledSkills; private String configDir; private Consumer onEvent; + private List commands; + private ElicitationHandler onElicitationRequest; /** * Gets the custom session ID. @@ -595,6 +597,56 @@ public SessionConfig setOnEvent(Consumer onEvent) { return this; } + /** + * Gets the slash commands registered for this session. + * + * @return the list of command definitions, or {@code null} + */ + public List getCommands() { + return commands == null ? null : Collections.unmodifiableList(commands); + } + + /** + * Sets slash commands registered for this session. + *

+ * When the CLI has a TUI, each command appears as {@code /name} for the user to + * invoke. The handler is called when the user executes the command. + * + * @param commands + * the list of command definitions + * @return this config instance for method chaining + * @see CommandDefinition + */ + public SessionConfig setCommands(List commands) { + this.commands = commands; + return this; + } + + /** + * Gets the elicitation request handler. + * + * @return the elicitation handler, or {@code null} + */ + public ElicitationHandler getOnElicitationRequest() { + return onElicitationRequest; + } + + /** + * Sets a handler for elicitation requests from the server or MCP tools. + *

+ * When provided, the server will route elicitation requests to this handler and + * report elicitation as a supported capability. + * + * @param onElicitationRequest + * the elicitation handler + * @return this config instance for method chaining + * @see ElicitationHandler + */ + public SessionConfig setOnElicitationRequest(ElicitationHandler onElicitationRequest) { + this.onElicitationRequest = onElicitationRequest; + return this; + } + /** * Creates a shallow clone of this {@code SessionConfig} instance. *

@@ -631,6 +683,8 @@ public SessionConfig clone() { copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.configDir = this.configDir; copy.onEvent = this.onEvent; + copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; + copy.onElicitationRequest = this.onElicitationRequest; return copy; } } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java new file mode 100644 index 000000000..bfc9fc161 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Provides UI methods for eliciting information from the user during a session. + *

+ * All methods on this interface throw {@link IllegalStateException} if the host + * does not report elicitation support via + * {@link com.github.copilot.sdk.CopilotSession#getCapabilities()}. Check + * {@code session.getCapabilities().getUi()?.getElicitation() == true} before + * calling. + * + *

Example Usage

+ * + *
{@code
+ * if (Boolean.TRUE
+ * 		.equals(session.getCapabilities().getUi() != null && session.getCapabilities().getUi().getElicitation())) {
+ * 	boolean confirmed = session.getUi().confirm("Are you sure?").get();
+ * }
+ * }
+ * + * @see com.github.copilot.sdk.CopilotSession#getUi() + * @since 1.0.0 + */ +public interface SessionUiApi { + + /** + * Shows a generic elicitation dialog with a custom schema. + * + * @param params + * the elicitation parameters including message and schema + * @return a future that resolves with the {@link ElicitationResult} + * @throws IllegalStateException + * if the host does not support elicitation + */ + CompletableFuture elicitation(ElicitationParams params); + + /** + * Shows a confirmation dialog and returns the user's boolean answer. + *

+ * Returns {@code false} if the user declines or cancels. + * + * @param message + * the message to display + * @return a future that resolves to {@code true} if the user confirmed + * @throws IllegalStateException + * if the host does not support elicitation + */ + CompletableFuture confirm(String message); + + /** + * Shows a selection dialog with the given options. + *

+ * Returns the selected value, or {@code null} if the user declines/cancels. + * + * @param message + * the message to display + * @param options + * the options to present + * @return a future that resolves to the selected string, or {@code null} + * @throws IllegalStateException + * if the host does not support elicitation + */ + CompletableFuture select(String message, String[] options); + + /** + * Shows a text input dialog. + *

+ * Returns the entered text, or {@code null} if the user declines/cancels. + * + * @param message + * the message to display + * @param options + * optional input field options, or {@code null} + * @return a future that resolves to the entered string, or {@code null} + * @throws IllegalStateException + * if the host does not support elicitation + */ + CompletableFuture input(String message, InputOptions options); +} diff --git a/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java b/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java new file mode 100644 index 000000000..9b8e0b587 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * UI-specific capability flags for a session. + * + * @since 1.0.0 + */ +public class SessionUiCapabilities { + + private Boolean elicitation; + + /** + * Returns whether the host supports interactive elicitation dialogs. + * + * @return {@code true} if elicitation is supported, {@code false} or + * {@code null} otherwise + */ + public Boolean getElicitation() { + return elicitation; + } + + /** + * Sets whether the host supports interactive elicitation dialogs. + * + * @param elicitation + * {@code true} if elicitation is supported + * @return this instance for method chaining + */ + public SessionUiCapabilities setElicitation(Boolean elicitation) { + this.elicitation = elicitation; + return this; + } +} From 4001135367c73413f74b4f6e4cd8c3490b4b4cda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:58:53 +0000 Subject: [PATCH 06/23] Add tests, documentation, and update .lastmerge to f7fd757 Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/16d575c4-83f4-4d20-99d9-b48635b3791d Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .lastmerge | 2 +- README.md | 2 +- src/site/markdown/advanced.md | 137 +++++++++++++ src/site/markdown/index.md | 2 +- .../com/github/copilot/sdk/CommandsTest.java | 138 +++++++++++++ .../copilot/sdk/CopilotSessionTest.java | 28 +++ .../github/copilot/sdk/ElicitationTest.java | 191 ++++++++++++++++++ 7 files changed, 497 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/github/copilot/sdk/CommandsTest.java create mode 100644 src/test/java/com/github/copilot/sdk/ElicitationTest.java diff --git a/.lastmerge b/.lastmerge index a0cf76b72..0d0067b5b 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -40887393a9e687dacc141a645799441b0313ff15 +f7fd7577109d64e261456b16c49baa56258eae4e diff --git a/README.md b/README.md index 3010b6839..f71424950 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A ### Requirements - Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start). -- GitHub Copilot 1.0.15-0 or later installed and in `PATH` (or provide custom `cliPath`) +- GitHub Copilot 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`) ### Maven diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index bc9302840..598acc2f7 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -1093,6 +1093,143 @@ See [TelemetryConfig](apidocs/com/github/copilot/sdk/json/TelemetryConfig.html) --- +## Slash Commands + +Register custom slash commands that users can invoke from the CLI TUI with `/commandname`. + +### Registering Commands + +```java +var config = new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCommands(List.of( + new CommandDefinition() + .setName("deploy") + .setDescription("Deploy the current branch") + .setHandler(context -> { + System.out.println("Deploying with args: " + context.getArgs()); + // perform deployment ... + return CompletableFuture.completedFuture(null); + }), + new CommandDefinition() + .setName("rollback") + .setDescription("Roll back the last deployment") + .setHandler(context -> { + // perform rollback ... + return CompletableFuture.completedFuture(null); + }) + )); + +try (CopilotClient client = new CopilotClient()) { + client.start().get(); + var session = client.createSession(config).get(); + // Users can now type /deploy or /rollback in the TUI +} +``` + +Each `CommandDefinition` requires a `name` (without the leading `/`), an optional `description` shown in the TUI's command completion UI, and a `CommandHandler` that is invoked when the user executes the command. + +The `CommandContext` passed to the handler provides: +- `getSessionId()` — the ID of the session where the command was invoked +- `getCommand()` — the full command text (e.g., `/deploy production`) +- `getCommandName()` — command name without the leading `/` (e.g., `deploy`) +- `getArgs()` — the argument string after the command name (e.g., `production`) + +--- + +## Elicitation (UI Dialogs) + +Elicitation allows your application to present structured UI dialogs to the user. There are two directions: + +1. **Incoming** — The server or an MCP tool requests input from the user via your `onElicitationRequest` handler. +2. **Outgoing** — Your session-side code proactively requests input via `session.getUi()`. + +### Incoming Elicitation Handler + +Register a handler to receive elicitation requests from the server: + +```java +var config = new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(context -> { + System.out.println("Elicitation request: " + context.getMessage()); + // Show the form to the user ... + var content = Map.of("confirmed", true); + return CompletableFuture.completedFuture( + new ElicitationResult() + .setAction(ElicitationResultAction.ACCEPT) + .setContent(content) + ); + }); +``` + +When `onElicitationRequest` is set, the SDK reports elicitation as a supported capability and the server will route elicitation requests to your handler. + +### Session Capabilities + +After `createSession` or `resumeSession`, check `session.getCapabilities()` to see what the host supports: + +```java +var session = client.createSession(config).get(); + +var caps = session.getCapabilities(); +if (caps.getUi() != null && Boolean.TRUE.equals(caps.getUi().getElicitation())) { + System.out.println("Elicitation is supported"); +} +``` + +Capabilities are updated in real time when a `capabilities.changed` event is received. + +### Outgoing Elicitation via `session.getUi()` + +If the host reports elicitation support, you can call the convenience methods on `session.getUi()`: + +```java +var ui = session.getUi(); + +// Boolean confirmation +boolean confirmed = ui.confirm("Are you sure you want to proceed?").get(); + +// Selection from options +String choice = ui.select("Choose an environment", new String[]{"dev", "staging", "prod"}).get(); + +// Text input +String value = ui.input("Enter your name", null).get(); + +// Custom schema +var result = ui.elicitation(new ElicitationParams() + .setMessage("Enter deployment details") + .setRequestedSchema(new ElicitationSchema() + .setProperties(Map.of( + "branch", Map.of("type", "string"), + "environment", Map.of("type", "string", "enum", List.of("dev", "staging", "prod")) + )) + .setRequired(List.of("branch", "environment")) + )).get(); +``` + +All `getUi()` methods throw `IllegalStateException` if the host does not support elicitation. Always check capabilities first. + +--- + +## Getting Session Metadata by ID + +Retrieve metadata for a specific session without listing all sessions: + +```java +SessionMetadata metadata = client.getSessionMetadata("session-123").get(); +if (metadata != null) { + System.out.println("Session: " + metadata.getSessionId()); + System.out.println("Started: " + metadata.getStartTime()); +} else { + System.out.println("Session not found"); +} +``` + +This is more efficient than `listSessions()` when you already know the session ID, as it performs a direct O(1) lookup instead of scanning all sessions. + +--- + ## Next Steps - 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 2f93c4ce9..b599484d9 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -9,7 +9,7 @@ Welcome to the documentation for the **GitHub Copilot SDK for Java** — a Java ### Requirements - Java 17 or later -- GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`) +- GitHub Copilot CLI 1.0.17 or later installed and in PATH (or provide custom `cliPath`) ### Installation diff --git a/src/test/java/com/github/copilot/sdk/CommandsTest.java b/src/test/java/com/github/copilot/sdk/CommandsTest.java new file mode 100644 index 000000000..dad26afbb --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/CommandsTest.java @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CommandContext; +import com.github.copilot.sdk.json.CommandDefinition; +import com.github.copilot.sdk.json.CommandHandler; +import com.github.copilot.sdk.json.CommandWireDefinition; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Unit tests for the Commands feature (CommandDefinition, CommandContext, + * SessionConfig.commands, ResumeSessionConfig.commands, and the wire + * representation). + * + *

+ * Ported from {@code CommandsTests.cs} in the upstream dotnet SDK. + *

+ */ +class CommandsTest { + + @Test + void commandDefinitionHasRequiredProperties() { + CommandHandler handler = context -> CompletableFuture.completedFuture(null); + var cmd = new CommandDefinition().setName("deploy").setDescription("Deploy the app").setHandler(handler); + + assertEquals("deploy", cmd.getName()); + assertEquals("Deploy the app", cmd.getDescription()); + assertNotNull(cmd.getHandler()); + } + + @Test + void commandContextHasAllProperties() { + var ctx = new CommandContext().setSessionId("session-1").setCommand("/deploy production") + .setCommandName("deploy").setArgs("production"); + + assertEquals("session-1", ctx.getSessionId()); + assertEquals("/deploy production", ctx.getCommand()); + assertEquals("deploy", ctx.getCommandName()); + assertEquals("production", ctx.getArgs()); + } + + @Test + void sessionConfigCommandsAreCloned() { + CommandHandler handler = ctx -> CompletableFuture.completedFuture(null); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCommands(List.of(new CommandDefinition().setName("deploy").setHandler(handler))); + + var clone = config.clone(); + + assertNotNull(clone.getCommands()); + assertEquals(1, clone.getCommands().size()); + assertEquals("deploy", clone.getCommands().get(0).getName()); + + // Collections should be independent — clone list is a copy + assertNotSame(config.getCommands(), clone.getCommands()); + } + + @Test + void resumeConfigCommandsAreCloned() { + CommandHandler handler = ctx -> CompletableFuture.completedFuture(null); + var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCommands(List.of(new CommandDefinition().setName("deploy").setHandler(handler))); + + var clone = config.clone(); + + assertNotNull(clone.getCommands()); + assertEquals(1, clone.getCommands().size()); + assertEquals("deploy", clone.getCommands().get(0).getName()); + } + + @Test + void buildCreateRequestIncludesCommandWireDefinitions() { + CommandHandler handler = ctx -> CompletableFuture.completedFuture(null); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCommands( + List.of(new CommandDefinition().setName("deploy").setDescription("Deploy").setHandler(handler), + new CommandDefinition().setName("rollback").setHandler(handler))); + + var request = SessionRequestBuilder.buildCreateRequest(config); + + assertNotNull(request.getCommands()); + assertEquals(2, request.getCommands().size()); + assertEquals("deploy", request.getCommands().get(0).getName()); + assertEquals("Deploy", request.getCommands().get(0).getDescription()); + assertEquals("rollback", request.getCommands().get(1).getName()); + assertNull(request.getCommands().get(1).getDescription()); + } + + @Test + void buildResumeRequestIncludesCommandWireDefinitions() { + CommandHandler handler = ctx -> CompletableFuture.completedFuture(null); + var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCommands( + List.of(new CommandDefinition().setName("deploy").setDescription("Deploy").setHandler(handler))); + + var request = SessionRequestBuilder.buildResumeRequest("session-1", config); + + assertNotNull(request.getCommands()); + assertEquals(1, request.getCommands().size()); + assertEquals("deploy", request.getCommands().get(0).getName()); + assertEquals("Deploy", request.getCommands().get(0).getDescription()); + } + + @Test + void buildCreateRequestWithNoCommandsHasNullCommandsList() { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + + var request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getCommands()); + } + + @Test + void commandWireDefinitionHasNameAndDescription() { + var wire = new CommandWireDefinition("deploy", "Deploy the app"); + + assertEquals("deploy", wire.getName()); + assertEquals("Deploy the app", wire.getDescription()); + } + + @Test + void commandWireDefinitionNullDescriptionAllowed() { + var wire = new CommandWireDefinition("rollback", null); + + assertEquals("rollback", wire.getName()); + assertNull(wire.getDescription()); + } +} diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 787312cef..39406d260 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -821,4 +822,31 @@ void testSessionListFilterFluentAPI() throws Exception { session.close(); } } + + /** + * Verifies that getSessionMetadata returns metadata for a known session ID. + * + * @see Snapshot: session/should_get_session_metadata_by_id + */ + @Test + void testShouldGetSessionMetadataById() throws Exception { + ctx.configureForTest("session", "should_get_session_metadata_by_id"); + + try (CopilotClient client = ctx.createClient()) { + var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS); + + var metadata = client.getSessionMetadata(session.getSessionId()).get(30, TimeUnit.SECONDS); + assertNotNull(metadata, "Metadata should not be null for known session"); + assertEquals(session.getSessionId(), metadata.getSessionId(), "Metadata session ID should match"); + + // A non-existent session should return null + var notFound = client.getSessionMetadata("non-existent-session-id").get(30, TimeUnit.SECONDS); + assertNull(notFound, "Non-existent session should return null"); + + session.close(); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/ElicitationTest.java b/src/test/java/com/github/copilot/sdk/ElicitationTest.java new file mode 100644 index 000000000..330153dd2 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ElicitationTest.java @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.ElicitationContext; +import com.github.copilot.sdk.json.ElicitationHandler; +import com.github.copilot.sdk.json.ElicitationParams; +import com.github.copilot.sdk.json.ElicitationResult; +import com.github.copilot.sdk.json.ElicitationResultAction; +import com.github.copilot.sdk.json.ElicitationSchema; +import com.github.copilot.sdk.json.InputOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionCapabilities; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionUiCapabilities; + +/** + * Unit tests for the Elicitation feature and Session Capabilities. + * + *

+ * Ported from {@code ElicitationTests.cs} in the upstream dotnet SDK. + *

+ */ +class ElicitationTest { + + @Test + void sessionCapabilitiesTypesAreProperlyStructured() { + var capabilities = new SessionCapabilities().setUi(new SessionUiCapabilities().setElicitation(true)); + + assertNotNull(capabilities.getUi()); + assertTrue(capabilities.getUi().getElicitation()); + + // Test with null UI + var emptyCapabilities = new SessionCapabilities(); + assertNull(emptyCapabilities.getUi()); + assertNull(emptyCapabilities.getUi()); + } + + @Test + void defaultCapabilitiesAreEmpty() { + var capabilities = new SessionCapabilities(); + + assertNull(capabilities.getUi()); + } + + @Test + void elicitationResultActionValues() { + assertEquals("accept", ElicitationResultAction.ACCEPT.getValue()); + assertEquals("decline", ElicitationResultAction.DECLINE.getValue()); + assertEquals("cancel", ElicitationResultAction.CANCEL.getValue()); + } + + @Test + void elicitationResultHasActionAndContent() { + var content = Map.of("name", (Object) "Alice"); + var result = new ElicitationResult().setAction(ElicitationResultAction.ACCEPT).setContent(content); + + assertEquals(ElicitationResultAction.ACCEPT, result.getAction()); + assertEquals(content, result.getContent()); + } + + @Test + void elicitationSchemaHasTypeAndProperties() { + var properties = Map.of("name", (Object) Map.of("type", "string")); + var schema = new ElicitationSchema().setType("object").setProperties(properties).setRequired(List.of("name")); + + assertEquals("object", schema.getType()); + assertEquals(properties, schema.getProperties()); + assertEquals(List.of("name"), schema.getRequired()); + } + + @Test + void elicitationSchemaDefaultTypeIsObject() { + var schema = new ElicitationSchema(); + + assertEquals("object", schema.getType()); + } + + @Test + void elicitationContextHasAllProperties() { + var properties = Map.of("field", (Object) Map.of("type", "string")); + var schema = new ElicitationSchema().setProperties(properties); + + var ctx = new ElicitationContext().setSessionId("session-1").setMessage("Please enter your name") + .setRequestedSchema(schema).setMode("form").setElicitationSource("mcp-server").setUrl(null); + + assertEquals("session-1", ctx.getSessionId()); + assertEquals("Please enter your name", ctx.getMessage()); + assertEquals(schema, ctx.getRequestedSchema()); + assertEquals("form", ctx.getMode()); + assertEquals("mcp-server", ctx.getElicitationSource()); + assertNull(ctx.getUrl()); + } + + @Test + void elicitationParamsHasMessageAndSchema() { + var schema = new ElicitationSchema().setProperties(Map.of("field", (Object) Map.of("type", "string"))); + var params = new ElicitationParams().setMessage("Enter name").setRequestedSchema(schema); + + assertEquals("Enter name", params.getMessage()); + assertEquals(schema, params.getRequestedSchema()); + } + + @Test + void inputOptionsHasAllFields() { + var opts = new InputOptions().setTitle("My Title").setDescription("My Desc").setMinLength(1).setMaxLength(100) + .setFormat("email").setDefaultValue("default@example.com"); + + assertEquals("My Title", opts.getTitle()); + assertEquals("My Desc", opts.getDescription()); + assertEquals(1, opts.getMinLength()); + assertEquals(100, opts.getMaxLength()); + assertEquals("email", opts.getFormat()); + assertEquals("default@example.com", opts.getDefaultValue()); + } + + @Test + void sessionConfigOnElicitationRequestIsCloned() { + ElicitationHandler handler = ctx -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.ACCEPT)); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(handler); + + var clone = config.clone(); + + // Handler reference is shared (not deep-cloned), but the field is copied + assertNotNull(clone.getOnElicitationRequest()); + assertSame(handler, clone.getOnElicitationRequest()); + } + + @Test + void resumeConfigOnElicitationRequestIsCloned() { + ElicitationHandler handler = ctx -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.CANCEL)); + + var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(handler); + + var clone = config.clone(); + + assertNotNull(clone.getOnElicitationRequest()); + assertSame(handler, clone.getOnElicitationRequest()); + } + + @Test + void buildCreateRequestSetsRequestElicitationWhenHandlerPresent() { + ElicitationHandler handler = ctx -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.ACCEPT)); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(handler); + + var request = SessionRequestBuilder.buildCreateRequest(config); + + assertTrue(Boolean.TRUE.equals(request.getRequestElicitation())); + } + + @Test + void buildCreateRequestDoesNotSetRequestElicitationWhenNoHandler() { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + + var request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getRequestElicitation()); + } + + @Test + void buildResumeRequestSetsRequestElicitationWhenHandlerPresent() { + ElicitationHandler handler = ctx -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.ACCEPT)); + + var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(handler); + + var request = SessionRequestBuilder.buildResumeRequest("session-1", config); + + assertTrue(Boolean.TRUE.equals(request.getRequestElicitation())); + } +} From 2da6b5a8334f1fc9c696858401856e277d3e1948 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:01:31 +0000 Subject: [PATCH 07/23] Fix code review issues: duplicate assertion, copyright header, Javadoc example Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/16d575c4-83f4-4d20-99d9-b48635b3791d Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .../github/copilot/sdk/json/GetSessionMetadataResponse.java | 4 ++++ src/main/java/com/github/copilot/sdk/json/SessionUiApi.java | 4 ++-- src/test/java/com/github/copilot/sdk/ElicitationTest.java | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java b/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java index 87c686f58..eeceb4177 100644 --- a/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java @@ -1,3 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + package com.github.copilot.sdk.json; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java index bfc9fc161..6dc583457 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java @@ -18,8 +18,8 @@ *

Example Usage

* *
{@code
- * if (Boolean.TRUE
- * 		.equals(session.getCapabilities().getUi() != null && session.getCapabilities().getUi().getElicitation())) {
+ * var caps = session.getCapabilities();
+ * if (caps.getUi() != null && Boolean.TRUE.equals(caps.getUi().getElicitation())) {
  * 	boolean confirmed = session.getUi().confirm("Are you sure?").get();
  * }
  * }
diff --git a/src/test/java/com/github/copilot/sdk/ElicitationTest.java b/src/test/java/com/github/copilot/sdk/ElicitationTest.java index 330153dd2..d6e2ac0b5 100644 --- a/src/test/java/com/github/copilot/sdk/ElicitationTest.java +++ b/src/test/java/com/github/copilot/sdk/ElicitationTest.java @@ -44,7 +44,6 @@ void sessionCapabilitiesTypesAreProperlyStructured() { // Test with null UI var emptyCapabilities = new SessionCapabilities(); assertNull(emptyCapabilities.getUi()); - assertNull(emptyCapabilities.getUi()); } @Test From 7060eed9c996e58f9c3896a72e65fa108dfc509b Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 13:12:05 -0400 Subject: [PATCH 08/23] Update src/main/java/com/github/copilot/sdk/CopilotSession.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/com/github/copilot/sdk/CopilotSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 768f0adaa..23b1b5368 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -724,7 +724,7 @@ private void handleBroadcastEventAsync(AbstractSessionEvent event) { executePermissionAndRespondAsync(data.requestId(), data.permissionRequest(), handler); } else if (event instanceof CommandExecuteEvent cmdEvent) { var data = cmdEvent.getData(); - if (data == null || data.requestId() == null) { + if (data == null || data.requestId() == null || data.commandName() == null) { return; } executeCommandAndRespondAsync(data.requestId(), data.commandName(), data.command(), data.args()); From 0bd084c6a6c8c06a6becf63d8e2efc4d51b5ef68 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 31 Mar 2026 16:33:52 -0400 Subject: [PATCH 09/23] Reapply "On branch edburns/spotless-agentic-workflow-42" This reverts commit 05d06d97f398a092897725a7651af10f96a047da. --- .github/copilot-instructions.md | 12 ++++++++++++ .github/workflows/copilot-setup-steps.yml | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d7dafb081..e3a8eb275 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -244,6 +244,18 @@ This SDK is designed to be **lightweight with minimal dependencies**: 5. Check for security vulnerabilities 6. Get team approval for non-trivial additions +## Pre-commit Hooks and Formatting (Coding Agent) + +The repository has a pre-commit hook (`.githooks/pre-commit`) that is **automatically enabled** in the Copilot coding agent environment via `copilot-setup-steps.yml`. The hook runs `mvn spotless:check` on any commit that includes changes under `src/`. + +**If a commit fails due to the pre-commit hook:** + +1. Run `mvn spotless:apply` to auto-fix formatting issues. +2. Re-stage the changed files with `git add -u`. +3. Retry the commit. + +**Best practice:** Always run `mvn spotless:apply` before committing Java source changes to avoid hook failures in the first place. If you forget and the hook rejects the commit, follow the three steps above and continue. + ## Commit and PR Guidelines ### Commit Messages diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 6a0cdec5b..8d8aa75c9 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -41,6 +41,10 @@ jobs: distribution: 'temurin' cache: 'maven' + # Enable pre-commit hooks so Spotless formatting is enforced on every commit + - name: Enable pre-commit hooks + run: git config core.hooksPath .githooks + # Verify installations - name: Verify tool installations run: | @@ -50,4 +54,6 @@ jobs: java -version gh --version gh aw version + echo "--- Git hooks path ---" + git config core.hooksPath echo "✅ All tools installed successfully" From 99d456780806e148292e25b51d0f8449545c00bd Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 31 Mar 2026 16:33:56 -0400 Subject: [PATCH 10/23] Reapply "Update .github/workflows/copilot-setup-steps.yml" This reverts commit afc34c439dd3e393840fe3c213ccc4d7b12fed39. --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 8d8aa75c9..145629457 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -41,7 +41,7 @@ jobs: distribution: 'temurin' cache: 'maven' - # Enable pre-commit hooks so Spotless formatting is enforced on every commit + # Enable repository pre-commit hooks (including Spotless checks for relevant source changes) - name: Enable pre-commit hooks run: git config core.hooksPath .githooks From e665e2100ab90262ba9e3a4d2fc04ca1f45c339e Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 6 Apr 2026 13:27:33 -0400 Subject: [PATCH 11/23] On branch edburns/re-enable-pre-commit-hooks modified: .github/copilot-instructions.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply @Copilot review recommendation. > This new section says the pre-commit hook is “automatically enabled” via `copilot-setup-steps.yml`, but earlier in the same document (and in CONTRIBUTING/README) the hook is described as something developers must manually enable with `git config core.hooksPath .githooks`. Please clarify the scope (e.g., auto-enabled only in the Copilot coding agent environment; local development still requires manual enablement) to avoid conflicting guidance, and consider linking explicitly to `.github/workflows/copilot-setup-steps.yml` for precision. Signed-off-by: Ed Burns --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e3a8eb275..284e2b800 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -104,7 +104,7 @@ When porting from .NET: - 4-space indentation (enforced by Spotless with Eclipse formatter) - Fluent setter pattern for configuration classes (e.g., `new SessionConfig().setModel("gpt-5").setTools(tools)`) - Public APIs require Javadoc (enforced by Checkstyle, except `json` and `events` packages) -- Pre-commit hook runs `mvn spotless:check` - enable with: `git config core.hooksPath .githooks` +- Pre-commit hook runs `mvn spotless:check` - Must be manually enabled with: `git config core.hooksPath .githooks`, except in the Copilot coding agent environment. This hook is explicitly enabled in the Copilot coding agent environment. See [copilot-setup-steps.yml](workflows/copilot-setup-steps.yml). ### Handler Pattern From 8cb5827b9423bdf91fe0c1cf8ac0abb1fec53a11 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 13:40:27 -0400 Subject: [PATCH 12/23] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f71424950..677389c6e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A ### Requirements - Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start). -- GitHub Copilot 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`) +- GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`) ### Maven From e365e05e692ff57f6371c43837e8db7ec299e6a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:42:42 +0000 Subject: [PATCH 13/23] Fix invalid Java ?. operator in SessionUiApi Javadoc prose Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/b5d79053-ae26-4c98-9047-b2457c08b0b8 Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- src/main/java/com/github/copilot/sdk/json/SessionUiApi.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java index 6dc583457..f0a43f261 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java @@ -12,8 +12,9 @@ * All methods on this interface throw {@link IllegalStateException} if the host * does not report elicitation support via * {@link com.github.copilot.sdk.CopilotSession#getCapabilities()}. Check - * {@code session.getCapabilities().getUi()?.getElicitation() == true} before - * calling. + * {@code session.getCapabilities().getUi() != null && + * Boolean.TRUE.equals(session.getCapabilities().getUi().getElicitation())} + * before calling. * *

Example Usage

* From 8835bd0fe108bff43844c0b678a0143d735bcb17 Mon Sep 17 00:00:00 2001 From: brunoborges <129743+brunoborges@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:29:56 +0000 Subject: [PATCH 14/23] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 482adb13b..a310734d0 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -6,13 +6,13 @@ - + coverage coverage - 84.7% - 84.7% + 78.3% + 78.3% From 538f138ad33680771cdb132b1c5ea9d856d49fa4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:18:16 +0000 Subject: [PATCH 15/23] Initial plan From 2bd1bf82152887efe1d6e5c2edf2568faeb0b3d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:50:36 +0000 Subject: [PATCH 16/23] Add tests to increase JaCoCo coverage from 78.7% to 85.2% Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/ee2608f6-2d6c-477e-9f79-a2968dec2436 Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .../com/github/copilot/sdk/AgentInfoTest.java | 64 ++++++ .../copilot/sdk/CliServerManagerTest.java | 27 +++ .../com/github/copilot/sdk/CommandsTest.java | 18 ++ .../github/copilot/sdk/ConfigCloneTest.java | 96 +++++++++ .../copilot/sdk/DataObjectCoverageTest.java | 172 ++++++++++++++++ .../com/github/copilot/sdk/ModelInfoTest.java | 68 +++++++ .../copilot/sdk/RpcHandlerDispatcherTest.java | 48 +++++ .../copilot/sdk/SessionEventParserTest.java | 184 ++++++++++++++++++ .../sdk/SessionRequestBuilderTest.java | 95 +++++++++ .../copilot/sdk/TelemetryConfigTest.java | 77 ++++++++ 10 files changed, 849 insertions(+) create mode 100644 src/test/java/com/github/copilot/sdk/AgentInfoTest.java create mode 100644 src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java create mode 100644 src/test/java/com/github/copilot/sdk/ModelInfoTest.java create mode 100644 src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java diff --git a/src/test/java/com/github/copilot/sdk/AgentInfoTest.java b/src/test/java/com/github/copilot/sdk/AgentInfoTest.java new file mode 100644 index 000000000..0893773e7 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/AgentInfoTest.java @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.AgentInfo; + +/** + * Unit tests for {@link AgentInfo} getters, setters, and fluent chaining. + */ +class AgentInfoTest { + + @Test + void defaultValuesAreNull() { + var agent = new AgentInfo(); + assertNull(agent.getName()); + assertNull(agent.getDisplayName()); + assertNull(agent.getDescription()); + } + + @Test + void nameGetterSetter() { + var agent = new AgentInfo(); + agent.setName("coder"); + assertEquals("coder", agent.getName()); + } + + @Test + void displayNameGetterSetter() { + var agent = new AgentInfo(); + agent.setDisplayName("Code Assistant"); + assertEquals("Code Assistant", agent.getDisplayName()); + } + + @Test + void descriptionGetterSetter() { + var agent = new AgentInfo(); + agent.setDescription("Helps with coding tasks"); + assertEquals("Helps with coding tasks", agent.getDescription()); + } + + @Test + void fluentChainingReturnsThis() { + var agent = new AgentInfo().setName("coder").setDisplayName("Code Assistant") + .setDescription("Helps with coding tasks"); + + assertEquals("coder", agent.getName()); + assertEquals("Code Assistant", agent.getDisplayName()); + assertEquals("Helps with coding tasks", agent.getDescription()); + } + + @Test + void fluentChainingReturnsSameInstance() { + var agent = new AgentInfo(); + assertSame(agent, agent.setName("test")); + assertSame(agent, agent.setDisplayName("Test")); + assertSame(agent, agent.setDescription("A test agent")); + } +} diff --git a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java index f17201583..32257b0a5 100644 --- a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java +++ b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.TelemetryConfig; /** * Unit tests for {@link CliServerManager} covering parseCliUrl, @@ -212,4 +213,30 @@ void startCliServerWithNullCliPath() throws Exception { assertNotNull(e); } } + + @Test + void startCliServerWithTelemetryAllOptions() throws Exception { + // The telemetry env vars are applied before ProcessBuilder.start() + // so even with a nonexistent CLI path, the telemetry code path is exercised + var telemetry = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setFilePath("/tmp/telemetry.log") + .setExporterType("otlp-http").setSourceName("test-app").setCaptureContent(true); + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setTelemetry(telemetry) + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithTelemetryCaptureContentFalse() throws Exception { + // Test the false branch of getCaptureContent() + var telemetry = new TelemetryConfig().setCaptureContent(false); + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setTelemetry(telemetry) + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } } diff --git a/src/test/java/com/github/copilot/sdk/CommandsTest.java b/src/test/java/com/github/copilot/sdk/CommandsTest.java index dad26afbb..baf26b39b 100644 --- a/src/test/java/com/github/copilot/sdk/CommandsTest.java +++ b/src/test/java/com/github/copilot/sdk/CommandsTest.java @@ -135,4 +135,22 @@ void commandWireDefinitionNullDescriptionAllowed() { assertEquals("rollback", wire.getName()); assertNull(wire.getDescription()); } + + @Test + void commandWireDefinitionFluentSetters() { + var wire = new CommandWireDefinition(); + wire.setName("status"); + wire.setDescription("Show deployment status"); + + assertEquals("status", wire.getName()); + assertEquals("Show deployment status", wire.getDescription()); + } + + @Test + void commandWireDefinitionFluentSettersChaining() { + var wire = new CommandWireDefinition().setName("logs").setDescription("View application logs"); + + assertEquals("logs", wire.getName()); + assertEquals("View application logs", wire.getDescription()); + } } diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index bf4881d5c..f3787705f 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -17,10 +17,13 @@ import com.github.copilot.sdk.events.AbstractSessionEvent; import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.InfiniteSessionConfig; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.ModelInfo; import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SystemMessageConfig; +import com.github.copilot.sdk.json.TelemetryConfig; class ConfigCloneTest { @@ -193,4 +196,97 @@ void clonePreservesNullFields() { MessageOptions msgClone = msg.clone(); assertNull(msgClone.getMode()); } + + @Test + @SuppressWarnings("deprecation") + void copilotClientOptionsDeprecatedAutoRestart() { + CopilotClientOptions opts = new CopilotClientOptions(); + assertFalse(opts.isAutoRestart()); + opts.setAutoRestart(true); + assertTrue(opts.isAutoRestart()); + } + + @Test + void copilotClientOptionsSetCliArgsNullClearsExisting() { + CopilotClientOptions opts = new CopilotClientOptions(); + opts.setCliArgs(new String[]{"--flag1"}); + assertNotNull(opts.getCliArgs()); + + // Setting null should clear the existing array + opts.setCliArgs(null); + assertNotNull(opts.getCliArgs()); + assertEquals(0, opts.getCliArgs().length); + } + + @Test + void copilotClientOptionsSetEnvironmentNullClearsExisting() { + CopilotClientOptions opts = new CopilotClientOptions(); + opts.setEnvironment(Map.of("KEY", "VALUE")); + assertNotNull(opts.getEnvironment()); + + // Setting null should clear the existing map (clears in-place → returns empty + // map) + opts.setEnvironment(null); + var env = opts.getEnvironment(); + assertTrue(env == null || env.isEmpty()); + } + + @Test + @SuppressWarnings("deprecation") + void copilotClientOptionsDeprecatedGithubToken() { + CopilotClientOptions opts = new CopilotClientOptions(); + opts.setGithubToken("ghp_deprecated_token"); + assertEquals("ghp_deprecated_token", opts.getGithubToken()); + assertEquals("ghp_deprecated_token", opts.getGitHubToken()); + } + + @Test + void copilotClientOptionsSetTelemetry() { + var telemetry = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318"); + var opts = new CopilotClientOptions(); + opts.setTelemetry(telemetry); + assertSame(telemetry, opts.getTelemetry()); + } + + @Test + void copilotClientOptionsSetUseLoggedInUserNull() { + var opts = new CopilotClientOptions(); + opts.setUseLoggedInUser(null); + // null → Boolean.FALSE + assertEquals(Boolean.FALSE, opts.getUseLoggedInUser()); + } + + @Test + void resumeSessionConfigAllSetters() { + var config = new ResumeSessionConfig(); + + var sysMsg = new SystemMessageConfig(); + config.setSystemMessage(sysMsg); + assertSame(sysMsg, config.getSystemMessage()); + + config.setAvailableTools(List.of("bash", "read_file")); + assertEquals(List.of("bash", "read_file"), config.getAvailableTools()); + + config.setExcludedTools(List.of("write_file")); + assertEquals(List.of("write_file"), config.getExcludedTools()); + + config.setReasoningEffort("high"); + assertEquals("high", config.getReasoningEffort()); + + config.setWorkingDirectory("/project/src"); + assertEquals("/project/src", config.getWorkingDirectory()); + + config.setConfigDir("/home/user/.config/copilot"); + assertEquals("/home/user/.config/copilot", config.getConfigDir()); + + config.setSkillDirectories(List.of("/skills/custom")); + assertEquals(List.of("/skills/custom"), config.getSkillDirectories()); + + config.setDisabledSkills(List.of("some-skill")); + assertEquals(List.of("some-skill"), config.getDisabledSkills()); + + var infiniteConfig = new InfiniteSessionConfig().setEnabled(true); + config.setInfiniteSessions(infiniteConfig); + assertSame(infiniteConfig, config.getInfiniteSessions()); + } } diff --git a/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java b/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java new file mode 100644 index 000000000..203f5faed --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.sdk.json.GetForegroundSessionResponse; +import com.github.copilot.sdk.json.PermissionRequest; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PostToolUseHookInput; +import com.github.copilot.sdk.json.PostToolUseHookOutput; +import com.github.copilot.sdk.json.PreToolUseHookInput; +import com.github.copilot.sdk.json.PreToolUseHookOutput; +import com.github.copilot.sdk.json.SectionOverride; +import com.github.copilot.sdk.json.SetForegroundSessionResponse; +import com.github.copilot.sdk.json.ToolBinaryResult; +import com.github.copilot.sdk.json.ToolResultObject; + +/** + * Unit tests for various data transfer objects and record types that were + * missing coverage, including hook output factory methods, record constructors, + * and getters for hook inputs. + */ +class DataObjectCoverageTest { + + // ===== PreToolUseHookOutput factory methods ===== + + @Test + void preToolUseHookOutputDenyWithReason() { + var output = PreToolUseHookOutput.deny("Security policy violation"); + assertEquals("deny", output.permissionDecision()); + assertEquals("Security policy violation", output.permissionDecisionReason()); + assertNull(output.modifiedArgs()); + } + + @Test + void preToolUseHookOutputAsk() { + var output = PreToolUseHookOutput.ask(); + assertEquals("ask", output.permissionDecision()); + assertNull(output.permissionDecisionReason()); + } + + @Test + void preToolUseHookOutputWithModifiedArgs() { + ObjectNode args = JsonNodeFactory.instance.objectNode(); + args.put("path", "/safe/path"); + + var output = PreToolUseHookOutput.withModifiedArgs("allow", args); + assertEquals("allow", output.permissionDecision()); + assertEquals(args, output.modifiedArgs()); + } + + // ===== PostToolUseHookOutput record ===== + + @Test + void postToolUseHookOutputRecord() { + var output = new PostToolUseHookOutput(null, "Extra context", false); + assertNull(output.modifiedResult()); + assertEquals("Extra context", output.additionalContext()); + assertFalse(output.suppressOutput()); + } + + // ===== ToolBinaryResult record ===== + + @Test + void toolBinaryResultRecord() { + var result = new ToolBinaryResult("base64data==", "image/png", "image", "A chart"); + assertEquals("base64data==", result.data()); + assertEquals("image/png", result.mimeType()); + assertEquals("image", result.type()); + assertEquals("A chart", result.description()); + } + + // ===== GetForegroundSessionResponse record ===== + + @Test + void getForegroundSessionResponseRecord() { + var response = new GetForegroundSessionResponse("session-123", "/home/user/project"); + assertEquals("session-123", response.sessionId()); + assertEquals("/home/user/project", response.workspacePath()); + } + + // ===== SetForegroundSessionResponse record ===== + + @Test + void setForegroundSessionResponseRecord() { + var successResponse = new SetForegroundSessionResponse(true, null); + assertTrue(successResponse.success()); + assertNull(successResponse.error()); + + var errorResponse = new SetForegroundSessionResponse(false, "Session not found"); + assertFalse(errorResponse.success()); + assertEquals("Session not found", errorResponse.error()); + } + + // ===== ToolResultObject factory methods ===== + + @Test + void toolResultObjectErrorWithTextAndError() { + var result = ToolResultObject.error("partial output", "File not found"); + assertEquals("error", result.resultType()); + assertEquals("partial output", result.textResultForLlm()); + assertEquals("File not found", result.error()); + } + + @Test + void toolResultObjectFailure() { + var result = ToolResultObject.failure("Tool unavailable", "Unknown tool"); + assertEquals("failure", result.resultType()); + assertEquals("Tool unavailable", result.textResultForLlm()); + assertEquals("Unknown tool", result.error()); + } + + // ===== PermissionRequest additional setters ===== + + @Test + void permissionRequestSetExtensionData() { + var req = new PermissionRequest(); + req.setExtensionData(java.util.Map.of("key", "value")); + assertEquals("value", req.getExtensionData().get("key")); + } + + // ===== SectionOverride setContent ===== + + @Test + void sectionOverrideSetContent() { + var override = new SectionOverride(); + override.setContent("Custom content"); + assertEquals("Custom content", override.getContent()); + } + + // ===== PreToolUseHookInput getters ===== + + @Test + void preToolUseHookInputGetters() { + var input = new PreToolUseHookInput(); + // Default values + assertEquals(0L, input.getTimestamp()); + assertNull(input.getCwd()); + assertNull(input.getToolArgs()); + } + + // ===== PostToolUseHookInput getters ===== + + @Test + void postToolUseHookInputGetters() { + var input = new PostToolUseHookInput(); + // Default values + assertEquals(0L, input.getTimestamp()); + assertNull(input.getCwd()); + assertNull(input.getToolArgs()); + } + + // ===== PermissionRequestResult setRules ===== + + @Test + void permissionRequestResultSetRules() { + var result = new PermissionRequestResult().setKind("allow"); + var rules = new java.util.ArrayList(); + rules.add("bash:read"); + rules.add("bash:write"); + result.setRules(rules); + assertEquals(2, result.getRules().size()); + assertEquals("bash:read", result.getRules().get(0)); + } +} diff --git a/src/test/java/com/github/copilot/sdk/ModelInfoTest.java b/src/test/java/com/github/copilot/sdk/ModelInfoTest.java new file mode 100644 index 000000000..f36d0c4bd --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ModelInfoTest.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.ModelInfo; +import com.github.copilot.sdk.json.ModelSupports; +import com.github.copilot.sdk.json.SessionMetadata; + +/** + * Unit tests for {@link ModelInfo}, {@link ModelSupports}, and + * {@link SessionMetadata} getters and setters. + */ +class ModelInfoTest { + + @Test + void modelSupportsReasoningEffortGetterSetter() { + var supports = new ModelSupports(); + assertFalse(supports.isReasoningEffort()); + + supports.setReasoningEffort(true); + assertTrue(supports.isReasoningEffort()); + } + + @Test + void modelSupportsFluentChaining() { + var supports = new ModelSupports().setVision(true).setReasoningEffort(true); + assertTrue(supports.isVision()); + assertTrue(supports.isReasoningEffort()); + } + + @Test + void modelInfoSupportedReasoningEffortsGetterSetter() { + var model = new ModelInfo(); + assertNull(model.getSupportedReasoningEfforts()); + + model.setSupportedReasoningEfforts(List.of("low", "medium", "high")); + assertEquals(List.of("low", "medium", "high"), model.getSupportedReasoningEfforts()); + } + + @Test + void modelInfoDefaultReasoningEffortGetterSetter() { + var model = new ModelInfo(); + assertNull(model.getDefaultReasoningEffort()); + + model.setDefaultReasoningEffort("medium"); + assertEquals("medium", model.getDefaultReasoningEffort()); + } + + @Test + void sessionMetadataGettersAndSetters() { + var meta = new SessionMetadata(); + assertNull(meta.getStartTime()); + assertNull(meta.getModifiedTime()); + assertNull(meta.getSummary()); + assertFalse(meta.isRemote()); + + meta.setRemote(true); + assertTrue(meta.isRemote()); + } +} diff --git a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java index 79f5d7c7e..315a38b90 100644 --- a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java +++ b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java @@ -542,4 +542,52 @@ void hooksInvokeWithNoHooksRegistered() throws Exception { JsonNode output = response.get("result").get("output"); assertTrue(output == null || output.isNull(), "Output should be null when no hooks registered"); } + + // ===== systemMessage.transform tests ===== + + @Test + void systemMessageTransformWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.putObject("sections"); + + invokeHandler("systemMessage.transform", "40", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + } + + @Test + void systemMessageTransformWithNullSessionId() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + // sessionId omitted → null → session lookup returns null → error + params.putObject("sections"); + + invokeHandler("systemMessage.transform", "41", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + } + + @Test + void systemMessageTransformWithKnownSessionNoCallbacks() throws Exception { + // Session without transform callbacks returns the sections unchanged + createSession("s1"); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + ObjectNode sections = params.putObject("sections"); + ObjectNode sectionData = sections.putObject("identity"); + sectionData.put("content", "Original content"); + + invokeHandler("systemMessage.transform", "42", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("result")); + JsonNode resultSections = response.get("result").get("sections"); + assertNotNull(resultSections); + assertEquals("Original content", resultSections.get("identity").get("content").asText()); + } } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index 5898d5301..4a63bb243 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -2385,4 +2385,188 @@ void testParseSystemNotificationEvent() throws Exception { assertNotNull(event.getData()); assertTrue(event.getData().content().contains("Agent completed")); } + + @Test + void testParseCapabilitiesChangedEvent() throws Exception { + String json = """ + { + "type": "capabilities.changed", + "data": { + "ui": { + "elicitation": true + } + } + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(CapabilitiesChangedEvent.class, event); + assertEquals("capabilities.changed", event.getType()); + + var castedEvent = (CapabilitiesChangedEvent) event; + assertNotNull(castedEvent.getData()); + assertNotNull(castedEvent.getData().ui()); + assertTrue(castedEvent.getData().ui().elicitation()); + + // Verify setData round-trip + var newData = new CapabilitiesChangedEvent.CapabilitiesChangedData( + new CapabilitiesChangedEvent.CapabilitiesChangedUi(false)); + castedEvent.setData(newData); + assertFalse(castedEvent.getData().ui().elicitation()); + } + + @Test + void testParseCommandExecuteEvent() throws Exception { + String json = """ + { + "type": "command.execute", + "data": { + "requestId": "req-001", + "command": "/deploy production", + "commandName": "deploy", + "args": "production" + } + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(CommandExecuteEvent.class, event); + assertEquals("command.execute", event.getType()); + + var castedEvent = (CommandExecuteEvent) event; + assertNotNull(castedEvent.getData()); + assertEquals("req-001", castedEvent.getData().requestId()); + assertEquals("/deploy production", castedEvent.getData().command()); + assertEquals("deploy", castedEvent.getData().commandName()); + assertEquals("production", castedEvent.getData().args()); + + // Verify setData round-trip + castedEvent.setData(new CommandExecuteEvent.CommandExecuteData("req-002", "/rollback", "rollback", null)); + assertEquals("req-002", castedEvent.getData().requestId()); + } + + @Test + void testParseElicitationRequestedEvent() throws Exception { + String json = """ + { + "type": "elicitation.requested", + "data": { + "requestId": "elix-001", + "toolCallId": "tc-123", + "elicitationSource": "mcp_tool", + "message": "Please provide your name", + "mode": "form", + "requestedSchema": { + "type": "object", + "properties": { + "name": {"type": "string"} + }, + "required": ["name"] + }, + "url": null + } + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(ElicitationRequestedEvent.class, event); + assertEquals("elicitation.requested", event.getType()); + + var castedEvent = (ElicitationRequestedEvent) event; + assertNotNull(castedEvent.getData()); + assertEquals("elix-001", castedEvent.getData().requestId()); + assertEquals("tc-123", castedEvent.getData().toolCallId()); + assertEquals("mcp_tool", castedEvent.getData().elicitationSource()); + assertEquals("Please provide your name", castedEvent.getData().message()); + assertEquals("form", castedEvent.getData().mode()); + assertNotNull(castedEvent.getData().requestedSchema()); + assertEquals("object", castedEvent.getData().requestedSchema().type()); + assertNotNull(castedEvent.getData().requestedSchema().properties()); + assertNotNull(castedEvent.getData().requestedSchema().required()); + assertTrue(castedEvent.getData().requestedSchema().required().contains("name")); + + // Verify setData round-trip + castedEvent.setData(new ElicitationRequestedEvent.ElicitationRequestedData("elix-002", null, null, "Enter URL", + "url", null, "https://example.com")); + assertEquals("elix-002", castedEvent.getData().requestId()); + assertEquals("url", castedEvent.getData().mode()); + } + + @Test + void testParseSessionContextChangedEvent() throws Exception { + String json = """ + { + "type": "session.context_changed", + "data": { + "cwd": "/home/user/project", + "gitRoot": "/home/user/project", + "repository": "my-repo", + "branch": "main" + } + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionContextChangedEvent.class, event); + assertEquals("session.context_changed", event.getType()); + + var castedEvent = (SessionContextChangedEvent) event; + assertNotNull(castedEvent.getData()); + assertEquals("/home/user/project", castedEvent.getData().getCwd()); + + // Verify setData round-trip + castedEvent.setData(null); + assertNull(castedEvent.getData()); + } + + @Test + void testParseSessionTaskCompleteEvent() throws Exception { + String json = """ + { + "type": "session.task_complete", + "data": { + "summary": "Task completed successfully" + } + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionTaskCompleteEvent.class, event); + assertEquals("session.task_complete", event.getType()); + + var castedEvent = (SessionTaskCompleteEvent) event; + assertNotNull(castedEvent.getData()); + assertEquals("Task completed successfully", castedEvent.getData().summary()); + + // Verify setData round-trip + castedEvent.setData(new SessionTaskCompleteEvent.SessionTaskCompleteData("New summary")); + assertEquals("New summary", castedEvent.getData().summary()); + } + + @Test + void testParseSubagentDeselectedEvent() throws Exception { + String json = """ + { + "type": "subagent.deselected", + "data": {} + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SubagentDeselectedEvent.class, event); + assertEquals("subagent.deselected", event.getType()); + + var castedEvent = (SubagentDeselectedEvent) event; + assertNotNull(castedEvent.getData()); + + // Verify setData round-trip + castedEvent.setData(new SubagentDeselectedEvent.SubagentDeselectedData()); + assertNotNull(castedEvent.getData()); + } } diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index 1a64b7534..75457583e 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -13,6 +13,9 @@ import org.junit.jupiter.api.Test; import com.github.copilot.sdk.json.CreateSessionRequest; +import com.github.copilot.sdk.json.ElicitationHandler; +import com.github.copilot.sdk.json.ElicitationResult; +import com.github.copilot.sdk.json.ElicitationResultAction; import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.ResumeSessionRequest; import com.github.copilot.sdk.json.SessionConfig; @@ -305,4 +308,96 @@ void extractTransformCallbacks_customizeModeWithTransform_extractsCallbacks() { assertEquals(com.github.copilot.sdk.json.SectionOverrideAction.TRANSFORM, wireSection.getAction()); assertNull(wireSection.getTransform()); } + + @Test + @SuppressWarnings("deprecation") + void buildCreateRequestWithSessionId_usesProvidedSessionId() { + var config = new SessionConfig(); + config.setSessionId("my-session-id"); + + // The deprecated single-arg overload uses the sessionId from config when set + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals("my-session-id", request.getSessionId()); + } + + @Test + void configureSessionWithNullConfig_returnsEarly() { + // configureSession with null config should return without error + CopilotSession session = new CopilotSession("session-1", null); + // Covers the null config early-return branch (L219-220) + assertDoesNotThrow(() -> SessionRequestBuilder.configureSession(session, (SessionConfig) null)); + } + + @Test + void configureSessionWithCommands_registersCommands() { + CopilotSession session = new CopilotSession("session-1", null); + + var cmd = new com.github.copilot.sdk.json.CommandDefinition().setName("deploy") + .setHandler(ctx -> CompletableFuture.completedFuture(null)); + var config = new SessionConfig().setCommands(List.of(cmd)); + + // Covers config.getCommands() != null branch (L235-236) + SessionRequestBuilder.configureSession(session, config); + // If no exception thrown, the branch was covered + } + + @Test + void configureSessionWithElicitationHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + ElicitationHandler handler = (context) -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.CANCEL)); + var config = new SessionConfig().setOnElicitationRequest(handler); + + // Covers config.getOnElicitationRequest() != null branch (L238-239) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureSessionWithOnEvent_registersEventHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new SessionConfig().setOnEvent(event -> { + }); + + // Covers config.getOnEvent() != null branch (L241-242) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithCommands_registersCommands() { + CopilotSession session = new CopilotSession("session-1", null); + + var cmd = new com.github.copilot.sdk.json.CommandDefinition().setName("rollback") + .setHandler(ctx -> CompletableFuture.completedFuture(null)); + var config = new ResumeSessionConfig().setCommands(List.of(cmd)); + + // Covers ResumeSessionConfig.getCommands() != null branch (L271-272) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithElicitationHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + ElicitationHandler handler = (context) -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.CANCEL)); + var config = new ResumeSessionConfig().setOnElicitationRequest(handler); + + // Covers ResumeSessionConfig.getOnElicitationRequest() != null branch + // (L274-275) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithOnEvent_registersEventHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new ResumeSessionConfig().setOnEvent(event -> { + }); + + // Covers ResumeSessionConfig.getOnEvent() != null branch (L277-278) + SessionRequestBuilder.configureSession(session, config); + } } diff --git a/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java b/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java new file mode 100644 index 000000000..99b360d2d --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.TelemetryConfig; + +/** + * Unit tests for {@link TelemetryConfig} getters, setters, and fluent chaining. + */ +class TelemetryConfigTest { + + @Test + void defaultValuesAreNull() { + var config = new TelemetryConfig(); + assertNull(config.getOtlpEndpoint()); + assertNull(config.getFilePath()); + assertNull(config.getExporterType()); + assertNull(config.getSourceName()); + assertNull(config.getCaptureContent()); + } + + @Test + void otlpEndpointGetterSetter() { + var config = new TelemetryConfig(); + config.setOtlpEndpoint("http://localhost:4318"); + assertEquals("http://localhost:4318", config.getOtlpEndpoint()); + } + + @Test + void filePathGetterSetter() { + var config = new TelemetryConfig(); + config.setFilePath("/tmp/telemetry.log"); + assertEquals("/tmp/telemetry.log", config.getFilePath()); + } + + @Test + void exporterTypeGetterSetter() { + var config = new TelemetryConfig(); + config.setExporterType("otlp-http"); + assertEquals("otlp-http", config.getExporterType()); + } + + @Test + void sourceNameGetterSetter() { + var config = new TelemetryConfig(); + config.setSourceName("my-app"); + assertEquals("my-app", config.getSourceName()); + } + + @Test + void captureContentGetterSetter() { + var config = new TelemetryConfig(); + config.setCaptureContent(true); + assertTrue(config.getCaptureContent()); + + config.setCaptureContent(false); + assertFalse(config.getCaptureContent()); + } + + @Test + void fluentChainingReturnsThis() { + var config = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setFilePath("/tmp/spans.json") + .setExporterType("file").setSourceName("sdk-test").setCaptureContent(true); + + assertEquals("http://localhost:4318", config.getOtlpEndpoint()); + assertEquals("/tmp/spans.json", config.getFilePath()); + assertEquals("file", config.getExporterType()); + assertEquals("sdk-test", config.getSourceName()); + assertTrue(config.getCaptureContent()); + } +} From 2b24c6f639e1f51dd6228b6ef8ea4360f111ca41 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 18:30:36 -0400 Subject: [PATCH 17/23] Update Mockito to 5.23.0 for Java 25 support Mockito 5.17.0 bundled ByteBuddy 1.15.11 which does not support Java 25 class files, causing SchedulerShutdownRaceTest and ZeroTimeoutContractTest to fail with 'Mockito cannot mock this class'. - Upgrade mockito-core from 5.17.0 to 5.23.0 (includes ByteBuddy 1.17.5+) - Add JDK 21+ profile that passes -XX:+EnableDynamicAgentLoading to Surefire, allowing Mockito/ByteBuddy to attach at runtime (JEP 451) - Use a default-empty surefire.jvm.args property so JDK 17-20 are unaffected by the flag Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pom.xml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 45404d99a..43e36ec4d 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,8 @@ ${copilot.sdk.clone.dir}/test false + + @@ -89,7 +91,7 @@ org.mockito mockito-core - 5.17.0 + 5.23.0 test @@ -245,8 +247,8 @@ maven-surefire-plugin 3.5.4 - - ${testExecutionAgentArgs} + + ${testExecutionAgentArgs} ${surefire.jvm.args} ${copilot.tests.dir} ${copilot.sdk.clone.dir} @@ -543,6 +545,18 @@ + + + jdk21+ + + [21,) + + + -XX:+EnableDynamicAgentLoading + + skip-test-harness From cce87cb1caf9701753bf64ee25be76cc3156d224 Mon Sep 17 00:00:00 2001 From: brunoborges <129743+brunoborges@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:53:38 +0000 Subject: [PATCH 18/23] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index a310734d0..f1a7c5eb3 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -6,13 +6,13 @@ - + coverage coverage - 78.3% - 78.3% + 84.4% + 84.4% From 00a3aa58630e142c23b7c85a80e0caa91a2dcc79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:05:43 +0000 Subject: [PATCH 19/23] Initial plan From 7e440159b95bda22d9cee3239f96befe5cf848eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:28:30 +0000 Subject: [PATCH 20/23] Update .lastmerge to c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1 Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index 0d0067b5b..83feb636c 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -f7fd7577109d64e261456b16c49baa56258eae4e +c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1 From 1508b9f323dec865eadef9fb53c7512bbdda1b80 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 7 Apr 2026 09:37:25 -0400 Subject: [PATCH 21/23] Update documentation for PR #52 upstream sync features Add coverage for slash commands, elicitation (UI dialogs), session capabilities, SessionUiApi, and getSessionMetadata across: - CHANGELOG.md: Unreleased section with Added/Fixed entries - documentation.md: event types, SessionConfig reference, resume options - advanced.md: table of contents entries for new sections - site.xml: navigation menu items Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 22 +++++++++++++++++++++- src/site/markdown/advanced.md | 7 +++++++ src/site/markdown/documentation.md | 17 +++++++++++++++++ src/site/site.xml | 3 +++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e306db097..f5050a3d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Upstream sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15) +> **Upstream sync:** [`github/copilot-sdk@f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd7577109d64e261456b16c49baa56258eae4e) + +### Added + +- Slash commands — register `/command` handlers invoked from the CLI TUI via `SessionConfig.setCommands()` (upstream: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757)) +- `CommandDefinition`, `CommandContext`, `CommandHandler`, `CommandWireDefinition` — types for defining and handling slash commands +- `CommandExecuteEvent` — event dispatched when a registered slash command is executed +- Elicitation (UI dialogs) — incoming handler via `SessionConfig.setOnElicitationRequest()` and outgoing convenience methods via `session.getUi()` (upstream: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757)) +- `ElicitationContext`, `ElicitationHandler`, `ElicitationParams`, `ElicitationResult`, `ElicitationResultAction`, `ElicitationSchema`, `InputOptions` — types for elicitation +- `ElicitationRequestedEvent` — event dispatched when an elicitation request is received +- `SessionUiApi` — convenience API on `session.getUi()` for `confirm()`, `select()`, `input()`, and `elicitation()` calls +- `SessionCapabilities` and `SessionUiCapabilities` — session capability reporting populated from create/resume response +- `CapabilitiesChangedEvent` — event dispatched when session capabilities are updated +- `CopilotClient.getSessionMetadata(String)` — O(1) session lookup by ID +- `GetSessionMetadataResponse` — response type for `getSessionMetadata` + +### Fixed + +- Permission events already resolved by a pre-hook now short-circuit before invoking the client-side handler +- `SessionUiApi` Javadoc now uses valid Java null-check syntax instead of `?.` +- README updated to say "GitHub Copilot CLI 1.0.17" instead of "GitHub Copilot 1.0.17" ## [0.2.1-java.1] - 2026-04-02 diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 598acc2f7..5ae5c8f94 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -47,6 +47,13 @@ This guide covers advanced scenarios for extending and customizing your Copilot - [Custom Event Error Handler](#Custom_Event_Error_Handler) - [Event Error Policy](#Event_Error_Policy) - [OpenTelemetry](#OpenTelemetry) +- [Slash Commands](#Slash_Commands) + - [Registering Commands](#Registering_Commands) +- [Elicitation (UI Dialogs)](#Elicitation_UI_Dialogs) + - [Incoming Elicitation Handler](#Incoming_Elicitation_Handler) + - [Session Capabilities](#Session_Capabilities) + - [Outgoing Elicitation via session.getUi()](#Outgoing_Elicitation_via_session.getUi) +- [Getting Session Metadata by ID](#Getting_Session_Metadata_by_ID) --- diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 8a9f919ac..a96f66698 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -245,6 +245,19 @@ The SDK supports event types organized by category. All events extend `AbstractS |-------|-------------|-------------| | `CommandQueuedEvent` | `command.queued` | A command was queued for execution | | `CommandCompletedEvent` | `command.completed` | A queued command completed | +| `CommandExecuteEvent` | `command.execute` | A registered slash command was dispatched for execution | + +### Elicitation Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `ElicitationRequestedEvent` | `elicitation.requested` | An elicitation (UI dialog) request was received | + +### Capability Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `CapabilitiesChangedEvent` | `capabilities.changed` | Session capabilities were updated | ### Plan Mode Events @@ -633,6 +646,8 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `skillDirectories` | Directories to load skills from | | `disabledSkills` | Skills to disable | | `infiniteSessions` | Configure infinite session behavior | +| `commands` | Slash command definitions for the resumed session | +| `onElicitationRequest` | Handler for incoming elicitation requests | | `disableResume` | When `true`, resumes without emitting a `session.resume` event | | `onEvent` | Event handler registered before session resumption | @@ -691,6 +706,8 @@ Complete list of all `SessionConfig` options for `createSession()`: | `skillDirectories` | List<String> | Directories to load skills from | [Skills](advanced.html#Skills_Configuration) | | `disabledSkills` | List<String> | Skills to disable by name | [Skills](advanced.html#Skills_Configuration) | | `configDir` | String | Custom configuration directory | [Config Dir](advanced.html#Custom_Configuration_Directory) | +| `commands` | List<CommandDefinition> | Slash command definitions | [Slash Commands](advanced.html#Slash_Commands) | +| `onElicitationRequest` | ElicitationHandler | Handler for incoming elicitation requests | [Elicitation](advanced.html#Elicitation_UI_Dialogs) | | `onEvent` | Consumer<AbstractSessionEvent> | Event handler registered before session creation | [Early Event Registration](advanced.html#Early_Event_Registration) | ### Cloning SessionConfig diff --git a/src/site/site.xml b/src/site/site.xml index f89ebe076..d012c0335 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -59,6 +59,9 @@ + + + From 04d03a798acc69044aa5e6377334fe34e0cc952a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Apr 2026 14:13:12 +0000 Subject: [PATCH 22/23] docs: update version references to 0.2.2-java.1 --- CHANGELOG.md | 19 ++++++++++++++----- README.md | 4 ++-- src/site/markdown/cookbook/error-handling.md | 14 +++++++------- .../markdown/cookbook/managing-local-files.md | 4 ++-- .../markdown/cookbook/multiple-sessions.md | 4 ++-- .../markdown/cookbook/persisting-sessions.md | 6 +++--- .../markdown/cookbook/pr-visualization.md | 2 +- 7 files changed, 31 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5050a3d2..38f6e6589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Upstream sync:** [`github/copilot-sdk@f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd7577109d64e261456b16c49baa56258eae4e) +> **Upstream sync:** [`github/copilot-sdk@c3fa6cb`](https://github.com/github/copilot-sdk/commit/c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1) +## [0.2.2-java.1] - 2026-04-07 + +> **Upstream sync:** [`github/copilot-sdk@c3fa6cb`](https://github.com/github/copilot-sdk/commit/c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1) ### Added - Slash commands — register `/command` handlers invoked from the CLI TUI via `SessionConfig.setCommands()` (upstream: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757)) @@ -485,16 +488,22 @@ New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` - Pre-commit hook for Spotless code formatting - Comprehensive API documentation -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...HEAD +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD +[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD +[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 [0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...HEAD +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD +[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 [0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 [0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...HEAD +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD +[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 [0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 [0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0 [0.1.32-java.0]: https://github.com/github/copilot-sdk-java/compare/v1.0.11...v0.1.32-java.0 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...HEAD +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD +[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 [0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 [0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0 [0.1.32-java.0]: https://github.com/github/copilot-sdk-java/compare/v1.0.11...v0.1.32-java.0 diff --git a/README.md b/README.md index 677389c6e..539a33895 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A com.github copilot-sdk-java - 0.2.1-java.1 + 0.2.2-java.1 ``` @@ -60,7 +60,7 @@ Snapshot builds of the next development version are published to Maven Central S ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:0.2.1-java.1' +implementation 'com.github:copilot-sdk-java:0.2.2-java.1' ``` ## Quick Start diff --git a/src/site/markdown/cookbook/error-handling.md b/src/site/markdown/cookbook/error-handling.md index 5ee5ef2ca..4240dc1ff 100644 --- a/src/site/markdown/cookbook/error-handling.md +++ b/src/site/markdown/cookbook/error-handling.md @@ -30,7 +30,7 @@ jbang BasicErrorHandling.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -64,7 +64,7 @@ public class BasicErrorHandling { ## Handling specific error types ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import java.util.concurrent.ExecutionException; @@ -99,7 +99,7 @@ public class SpecificErrorHandling { ## Timeout handling ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotSession; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -130,7 +130,7 @@ public class TimeoutHandling { ## Aborting a request ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotSession; import com.github.copilot.sdk.json.MessageOptions; import java.util.concurrent.Executors; @@ -162,7 +162,7 @@ public class AbortRequest { ## Graceful shutdown ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; public class GracefulShutdown { @@ -192,7 +192,7 @@ public class GracefulShutdown { ## Try-with-resources pattern ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -224,7 +224,7 @@ public class TryWithResources { ## Handling tool errors ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; diff --git a/src/site/markdown/cookbook/managing-local-files.md b/src/site/markdown/cookbook/managing-local-files.md index aa9ba23bc..9535772b2 100644 --- a/src/site/markdown/cookbook/managing-local-files.md +++ b/src/site/markdown/cookbook/managing-local-files.md @@ -34,7 +34,7 @@ jbang ManagingLocalFiles.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.events.SessionIdleEvent; @@ -161,7 +161,7 @@ session.send(new MessageOptions().setPrompt(prompt)); ## Interactive file organization ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import java.io.BufferedReader; import java.io.InputStreamReader; diff --git a/src/site/markdown/cookbook/multiple-sessions.md b/src/site/markdown/cookbook/multiple-sessions.md index fe5c2f0d9..776b6db6d 100644 --- a/src/site/markdown/cookbook/multiple-sessions.md +++ b/src/site/markdown/cookbook/multiple-sessions.md @@ -30,7 +30,7 @@ jbang MultipleSessions.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -123,7 +123,7 @@ try { ## Managing session lifecycle with CompletableFuture ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import java.util.concurrent.CompletableFuture; import java.util.List; diff --git a/src/site/markdown/cookbook/persisting-sessions.md b/src/site/markdown/cookbook/persisting-sessions.md index e3fd11b13..e653b8a6a 100644 --- a/src/site/markdown/cookbook/persisting-sessions.md +++ b/src/site/markdown/cookbook/persisting-sessions.md @@ -30,7 +30,7 @@ jbang PersistingSessions.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -127,7 +127,7 @@ public class DeleteSession { ## Getting session history ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.events.UserMessageEvent; @@ -162,7 +162,7 @@ public class SessionHistory { ## Complete example with session management ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import java.util.Scanner; public class SessionManager { diff --git a/src/site/markdown/cookbook/pr-visualization.md b/src/site/markdown/cookbook/pr-visualization.md index dbd240a40..ad2939842 100644 --- a/src/site/markdown/cookbook/pr-visualization.md +++ b/src/site/markdown/cookbook/pr-visualization.md @@ -34,7 +34,7 @@ jbang PRVisualization.java github/copilot-sdk ## Full example: PRVisualization.java ```java -//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +//DEPS com.github:copilot-sdk-java:0.2.2-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.events.ToolExecutionStartEvent; From a86edb2891b5784c271f787c29ea368b12734f61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Apr 2026 14:13:34 +0000 Subject: [PATCH 23/23] [maven-release-plugin] prepare release v0.2.2-java.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 43e36ec4d..8e3e7e1df 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java - 0.2.2-java.1-SNAPSHOT + 0.2.2-java.1 jar GitHub Copilot SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk-java.git scm:git:https://github.com/github/copilot-sdk-java.git https://github.com/github/copilot-sdk-java - HEAD + v0.2.2-java.1