diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg
index 482adb13b..f1a7c5eb3 100644
--- a/.github/badges/jacoco.svg
+++ b/.github/badges/jacoco.svg
@@ -12,7 +12,7 @@
coveragecoverage
- 84.7%
- 84.7%
+ 84.4%
+ 84.4%
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index d7dafb081..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
@@ -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..145629457 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 repository pre-commit hooks (including Spotless checks for relevant source changes)
+ - 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"
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.
diff --git a/.lastmerge b/.lastmerge
index a0cf76b72..83feb636c 100644
--- a/.lastmerge
+++ b/.lastmerge
@@ -1 +1 @@
-40887393a9e687dacc141a645799441b0313ff15
+c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e306db097..38f6e6589 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,30 @@ 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@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))
+- `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
@@ -465,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 3010b6839..539a33895 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 CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`)
### Maven
@@ -33,7 +33,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A
com.githubcopilot-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/pom.xml b/pom.xml
index 43e1b436d..8e3e7e1df 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
com.githubcopilot-sdk-java
- 0.2.1-java.1
+ 0.2.2-java.1jarGitHub Copilot SDK :: Java
@@ -33,7 +33,7 @@
scm:git:https://github.com/github/copilot-sdk-java.gitscm:git:https://github.com/github/copilot-sdk-java.githttps://github.com/github/copilot-sdk-java
- v0.2.1-java.1
+ v0.2.2-java.1
@@ -51,6 +51,8 @@
${copilot.sdk.clone.dir}/testfalse
+
+
@@ -89,7 +91,7 @@
org.mockitomockito-core
- 5.17.0
+ 5.23.0test
@@ -245,8 +247,8 @@
maven-surefire-plugin3.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
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..23b1b5368 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.
*
+ * 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.
*
+ * 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.
+ *
+ *
+ *
+ * @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..eeceb4177
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java
@@ -0,0 +1,19 @@
+/*---------------------------------------------------------------------------------------------
+ * 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;
+
+/**
+ * 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..f0a43f261
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java
@@ -0,0 +1,86 @@
+/*---------------------------------------------------------------------------------------------
+ * 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() != null &&
+ * Boolean.TRUE.equals(session.getCapabilities().getUi().getElicitation())}
+ * before calling.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var caps = session.getCapabilities();
+ * if (caps.getUi() != null && Boolean.TRUE.equals(caps.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;
+ }
+}
diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md
index bc9302840..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)
---
@@ -1093,6 +1100,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/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;
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/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/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 @@
+
+
+
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
new file mode 100644
index 000000000..baf26b39b
--- /dev/null
+++ b/src/test/java/com/github/copilot/sdk/CommandsTest.java
@@ -0,0 +1,156 @@
+/*---------------------------------------------------------------------------------------------
+ * 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.
+ *