Skip to content

Commit 4001135

Browse files
Copilotedburns
andauthored
Add tests, documentation, and update .lastmerge to f7fd757
Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/16d575c4-83f4-4d20-99d9-b48635b3791d Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent cc84fab commit 4001135

File tree

7 files changed

+497
-3
lines changed

7 files changed

+497
-3
lines changed

.lastmerge

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
40887393a9e687dacc141a645799441b0313ff15
1+
f7fd7577109d64e261456b16c49baa56258eae4e

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A
2525
### Requirements
2626

2727
- Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start).
28-
- GitHub Copilot 1.0.15-0 or later installed and in `PATH` (or provide custom `cliPath`)
28+
- GitHub Copilot 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`)
2929

3030
### Maven
3131

src/site/markdown/advanced.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,143 @@ See [TelemetryConfig](apidocs/com/github/copilot/sdk/json/TelemetryConfig.html)
10931093

10941094
---
10951095

1096+
## Slash Commands
1097+
1098+
Register custom slash commands that users can invoke from the CLI TUI with `/commandname`.
1099+
1100+
### Registering Commands
1101+
1102+
```java
1103+
var config = new SessionConfig()
1104+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
1105+
.setCommands(List.of(
1106+
new CommandDefinition()
1107+
.setName("deploy")
1108+
.setDescription("Deploy the current branch")
1109+
.setHandler(context -> {
1110+
System.out.println("Deploying with args: " + context.getArgs());
1111+
// perform deployment ...
1112+
return CompletableFuture.completedFuture(null);
1113+
}),
1114+
new CommandDefinition()
1115+
.setName("rollback")
1116+
.setDescription("Roll back the last deployment")
1117+
.setHandler(context -> {
1118+
// perform rollback ...
1119+
return CompletableFuture.completedFuture(null);
1120+
})
1121+
));
1122+
1123+
try (CopilotClient client = new CopilotClient()) {
1124+
client.start().get();
1125+
var session = client.createSession(config).get();
1126+
// Users can now type /deploy or /rollback in the TUI
1127+
}
1128+
```
1129+
1130+
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.
1131+
1132+
The `CommandContext` passed to the handler provides:
1133+
- `getSessionId()` — the ID of the session where the command was invoked
1134+
- `getCommand()` — the full command text (e.g., `/deploy production`)
1135+
- `getCommandName()` — command name without the leading `/` (e.g., `deploy`)
1136+
- `getArgs()` — the argument string after the command name (e.g., `production`)
1137+
1138+
---
1139+
1140+
## Elicitation (UI Dialogs)
1141+
1142+
Elicitation allows your application to present structured UI dialogs to the user. There are two directions:
1143+
1144+
1. **Incoming** — The server or an MCP tool requests input from the user via your `onElicitationRequest` handler.
1145+
2. **Outgoing** — Your session-side code proactively requests input via `session.getUi()`.
1146+
1147+
### Incoming Elicitation Handler
1148+
1149+
Register a handler to receive elicitation requests from the server:
1150+
1151+
```java
1152+
var config = new SessionConfig()
1153+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
1154+
.setOnElicitationRequest(context -> {
1155+
System.out.println("Elicitation request: " + context.getMessage());
1156+
// Show the form to the user ...
1157+
var content = Map.of("confirmed", true);
1158+
return CompletableFuture.completedFuture(
1159+
new ElicitationResult()
1160+
.setAction(ElicitationResultAction.ACCEPT)
1161+
.setContent(content)
1162+
);
1163+
});
1164+
```
1165+
1166+
When `onElicitationRequest` is set, the SDK reports elicitation as a supported capability and the server will route elicitation requests to your handler.
1167+
1168+
### Session Capabilities
1169+
1170+
After `createSession` or `resumeSession`, check `session.getCapabilities()` to see what the host supports:
1171+
1172+
```java
1173+
var session = client.createSession(config).get();
1174+
1175+
var caps = session.getCapabilities();
1176+
if (caps.getUi() != null && Boolean.TRUE.equals(caps.getUi().getElicitation())) {
1177+
System.out.println("Elicitation is supported");
1178+
}
1179+
```
1180+
1181+
Capabilities are updated in real time when a `capabilities.changed` event is received.
1182+
1183+
### Outgoing Elicitation via `session.getUi()`
1184+
1185+
If the host reports elicitation support, you can call the convenience methods on `session.getUi()`:
1186+
1187+
```java
1188+
var ui = session.getUi();
1189+
1190+
// Boolean confirmation
1191+
boolean confirmed = ui.confirm("Are you sure you want to proceed?").get();
1192+
1193+
// Selection from options
1194+
String choice = ui.select("Choose an environment", new String[]{"dev", "staging", "prod"}).get();
1195+
1196+
// Text input
1197+
String value = ui.input("Enter your name", null).get();
1198+
1199+
// Custom schema
1200+
var result = ui.elicitation(new ElicitationParams()
1201+
.setMessage("Enter deployment details")
1202+
.setRequestedSchema(new ElicitationSchema()
1203+
.setProperties(Map.of(
1204+
"branch", Map.of("type", "string"),
1205+
"environment", Map.of("type", "string", "enum", List.of("dev", "staging", "prod"))
1206+
))
1207+
.setRequired(List.of("branch", "environment"))
1208+
)).get();
1209+
```
1210+
1211+
All `getUi()` methods throw `IllegalStateException` if the host does not support elicitation. Always check capabilities first.
1212+
1213+
---
1214+
1215+
## Getting Session Metadata by ID
1216+
1217+
Retrieve metadata for a specific session without listing all sessions:
1218+
1219+
```java
1220+
SessionMetadata metadata = client.getSessionMetadata("session-123").get();
1221+
if (metadata != null) {
1222+
System.out.println("Session: " + metadata.getSessionId());
1223+
System.out.println("Started: " + metadata.getStartTime());
1224+
} else {
1225+
System.out.println("Session not found");
1226+
}
1227+
```
1228+
1229+
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.
1230+
1231+
---
1232+
10961233
## Next Steps
10971234

10981235
- 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort

src/site/markdown/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Welcome to the documentation for the **GitHub Copilot SDK for Java** — a Java
99
### Requirements
1010

1111
- Java 17 or later
12-
- GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`)
12+
- GitHub Copilot CLI 1.0.17 or later installed and in PATH (or provide custom `cliPath`)
1313

1414
### Installation
1515

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
import java.util.List;
10+
import java.util.concurrent.CompletableFuture;
11+
12+
import org.junit.jupiter.api.Test;
13+
14+
import com.github.copilot.sdk.json.CommandContext;
15+
import com.github.copilot.sdk.json.CommandDefinition;
16+
import com.github.copilot.sdk.json.CommandHandler;
17+
import com.github.copilot.sdk.json.CommandWireDefinition;
18+
import com.github.copilot.sdk.json.PermissionHandler;
19+
import com.github.copilot.sdk.json.ResumeSessionConfig;
20+
import com.github.copilot.sdk.json.SessionConfig;
21+
22+
/**
23+
* Unit tests for the Commands feature (CommandDefinition, CommandContext,
24+
* SessionConfig.commands, ResumeSessionConfig.commands, and the wire
25+
* representation).
26+
*
27+
* <p>
28+
* Ported from {@code CommandsTests.cs} in the upstream dotnet SDK.
29+
* </p>
30+
*/
31+
class CommandsTest {
32+
33+
@Test
34+
void commandDefinitionHasRequiredProperties() {
35+
CommandHandler handler = context -> CompletableFuture.completedFuture(null);
36+
var cmd = new CommandDefinition().setName("deploy").setDescription("Deploy the app").setHandler(handler);
37+
38+
assertEquals("deploy", cmd.getName());
39+
assertEquals("Deploy the app", cmd.getDescription());
40+
assertNotNull(cmd.getHandler());
41+
}
42+
43+
@Test
44+
void commandContextHasAllProperties() {
45+
var ctx = new CommandContext().setSessionId("session-1").setCommand("/deploy production")
46+
.setCommandName("deploy").setArgs("production");
47+
48+
assertEquals("session-1", ctx.getSessionId());
49+
assertEquals("/deploy production", ctx.getCommand());
50+
assertEquals("deploy", ctx.getCommandName());
51+
assertEquals("production", ctx.getArgs());
52+
}
53+
54+
@Test
55+
void sessionConfigCommandsAreCloned() {
56+
CommandHandler handler = ctx -> CompletableFuture.completedFuture(null);
57+
var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
58+
.setCommands(List.of(new CommandDefinition().setName("deploy").setHandler(handler)));
59+
60+
var clone = config.clone();
61+
62+
assertNotNull(clone.getCommands());
63+
assertEquals(1, clone.getCommands().size());
64+
assertEquals("deploy", clone.getCommands().get(0).getName());
65+
66+
// Collections should be independent — clone list is a copy
67+
assertNotSame(config.getCommands(), clone.getCommands());
68+
}
69+
70+
@Test
71+
void resumeConfigCommandsAreCloned() {
72+
CommandHandler handler = ctx -> CompletableFuture.completedFuture(null);
73+
var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
74+
.setCommands(List.of(new CommandDefinition().setName("deploy").setHandler(handler)));
75+
76+
var clone = config.clone();
77+
78+
assertNotNull(clone.getCommands());
79+
assertEquals(1, clone.getCommands().size());
80+
assertEquals("deploy", clone.getCommands().get(0).getName());
81+
}
82+
83+
@Test
84+
void buildCreateRequestIncludesCommandWireDefinitions() {
85+
CommandHandler handler = ctx -> CompletableFuture.completedFuture(null);
86+
var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCommands(
87+
List.of(new CommandDefinition().setName("deploy").setDescription("Deploy").setHandler(handler),
88+
new CommandDefinition().setName("rollback").setHandler(handler)));
89+
90+
var request = SessionRequestBuilder.buildCreateRequest(config);
91+
92+
assertNotNull(request.getCommands());
93+
assertEquals(2, request.getCommands().size());
94+
assertEquals("deploy", request.getCommands().get(0).getName());
95+
assertEquals("Deploy", request.getCommands().get(0).getDescription());
96+
assertEquals("rollback", request.getCommands().get(1).getName());
97+
assertNull(request.getCommands().get(1).getDescription());
98+
}
99+
100+
@Test
101+
void buildResumeRequestIncludesCommandWireDefinitions() {
102+
CommandHandler handler = ctx -> CompletableFuture.completedFuture(null);
103+
var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCommands(
104+
List.of(new CommandDefinition().setName("deploy").setDescription("Deploy").setHandler(handler)));
105+
106+
var request = SessionRequestBuilder.buildResumeRequest("session-1", config);
107+
108+
assertNotNull(request.getCommands());
109+
assertEquals(1, request.getCommands().size());
110+
assertEquals("deploy", request.getCommands().get(0).getName());
111+
assertEquals("Deploy", request.getCommands().get(0).getDescription());
112+
}
113+
114+
@Test
115+
void buildCreateRequestWithNoCommandsHasNullCommandsList() {
116+
var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL);
117+
118+
var request = SessionRequestBuilder.buildCreateRequest(config);
119+
120+
assertNull(request.getCommands());
121+
}
122+
123+
@Test
124+
void commandWireDefinitionHasNameAndDescription() {
125+
var wire = new CommandWireDefinition("deploy", "Deploy the app");
126+
127+
assertEquals("deploy", wire.getName());
128+
assertEquals("Deploy the app", wire.getDescription());
129+
}
130+
131+
@Test
132+
void commandWireDefinitionNullDescriptionAllowed() {
133+
var wire = new CommandWireDefinition("rollback", null);
134+
135+
assertEquals("rollback", wire.getName());
136+
assertNull(wire.getDescription());
137+
}
138+
}

src/test/java/com/github/copilot/sdk/CopilotSessionTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static org.junit.jupiter.api.Assertions.assertEquals;
88
import static org.junit.jupiter.api.Assertions.assertFalse;
99
import static org.junit.jupiter.api.Assertions.assertNotNull;
10+
import static org.junit.jupiter.api.Assertions.assertNull;
1011
import static org.junit.jupiter.api.Assertions.assertTrue;
1112
import static org.junit.jupiter.api.Assertions.fail;
1213

@@ -821,4 +822,31 @@ void testSessionListFilterFluentAPI() throws Exception {
821822
session.close();
822823
}
823824
}
825+
826+
/**
827+
* Verifies that getSessionMetadata returns metadata for a known session ID.
828+
*
829+
* @see Snapshot: session/should_get_session_metadata_by_id
830+
*/
831+
@Test
832+
void testShouldGetSessionMetadataById() throws Exception {
833+
ctx.configureForTest("session", "should_get_session_metadata_by_id");
834+
835+
try (CopilotClient client = ctx.createClient()) {
836+
var session = client
837+
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
838+
839+
session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS);
840+
841+
var metadata = client.getSessionMetadata(session.getSessionId()).get(30, TimeUnit.SECONDS);
842+
assertNotNull(metadata, "Metadata should not be null for known session");
843+
assertEquals(session.getSessionId(), metadata.getSessionId(), "Metadata session ID should match");
844+
845+
// A non-existent session should return null
846+
var notFound = client.getSessionMetadata("non-existent-session-id").get(30, TimeUnit.SECONDS);
847+
assertNull(notFound, "Non-existent session should return null");
848+
849+
session.close();
850+
}
851+
}
824852
}

0 commit comments

Comments
 (0)