Skip to content

Commit 45b836e

Browse files
committed
Add foreground session methods and lifecycle event subscription to CopilotClient
1 parent ba6bc99 commit 45b836e

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

src/main/java/com/github/copilot/sdk/CopilotClient.java

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ public class CopilotClient implements AutoCloseable {
9191
private final Integer optionsPort;
9292
private volatile List<ModelInfo> modelsCache;
9393
private final Object modelsCacheLock = new Object();
94+
private final List<com.github.copilot.sdk.json.SessionLifecycleHandler> lifecycleHandlers = new ArrayList<>();
95+
private final Map<String, List<com.github.copilot.sdk.json.SessionLifecycleHandler>> typedLifecycleHandlers = new ConcurrentHashMap<>();
96+
private final Object lifecycleHandlersLock = new Object();
9497

9598
/**
9699
* Creates a new CopilotClient with default options.
@@ -218,6 +221,28 @@ private void registerRpcHandlers(JsonRpcClient rpc) {
218221
}
219222
});
220223

224+
// Handle session lifecycle events
225+
rpc.registerMethodHandler("session.lifecycle", (requestId, params) -> {
226+
try {
227+
String type = params.has("type") ? params.get("type").asText() : "";
228+
String sessionId = params.has("sessionId") ? params.get("sessionId").asText() : "";
229+
230+
com.github.copilot.sdk.json.SessionLifecycleEvent event = new com.github.copilot.sdk.json.SessionLifecycleEvent();
231+
event.setType(type);
232+
event.setSessionId(sessionId);
233+
234+
if (params.has("metadata") && !params.get("metadata").isNull()) {
235+
com.github.copilot.sdk.json.SessionLifecycleEventMetadata metadata = MAPPER.treeToValue(
236+
params.get("metadata"), com.github.copilot.sdk.json.SessionLifecycleEventMetadata.class);
237+
event.setMetadata(metadata);
238+
}
239+
240+
dispatchLifecycleEvent(event);
241+
} catch (Exception e) {
242+
LOG.log(Level.SEVERE, "Error handling session lifecycle event", e);
243+
}
244+
});
245+
221246
// Handle tool calls
222247
rpc.registerMethodHandler("tool.call", (requestId, params) -> {
223248
handleToolCall(rpc, requestId, params);
@@ -805,6 +830,124 @@ public CompletableFuture<List<SessionMetadata>> listSessions() {
805830
.thenApply(ListSessionsResponse::getSessions));
806831
}
807832

833+
/**
834+
* Gets the ID of the session currently displayed in the TUI.
835+
* <p>
836+
* This is only available when connecting to a server running in TUI+server mode
837+
* (--ui-server).
838+
*
839+
* @return a future that resolves with the session ID, or null if no foreground
840+
* session is set
841+
*/
842+
public CompletableFuture<String> getForegroundSessionId() {
843+
return ensureConnected().thenCompose(connection -> connection.rpc
844+
.invoke("session.getForeground", Map.of(),
845+
com.github.copilot.sdk.json.GetForegroundSessionResponse.class)
846+
.thenApply(com.github.copilot.sdk.json.GetForegroundSessionResponse::getSessionId));
847+
}
848+
849+
/**
850+
* Requests the TUI to switch to displaying the specified session.
851+
* <p>
852+
* This is only available when connecting to a server running in TUI+server mode
853+
* (--ui-server).
854+
*
855+
* @param sessionId
856+
* the ID of the session to display in the TUI
857+
* @return a future that completes when the operation is done
858+
* @throws RuntimeException
859+
* if the operation fails
860+
*/
861+
public CompletableFuture<Void> setForegroundSessionId(String sessionId) {
862+
return ensureConnected()
863+
.thenCompose(
864+
connection -> connection.rpc
865+
.invoke("session.setForeground", Map.of("sessionId", sessionId),
866+
com.github.copilot.sdk.json.SetForegroundSessionResponse.class)
867+
.thenAccept(response -> {
868+
if (!response.isSuccess()) {
869+
throw new RuntimeException(response.getError() != null
870+
? response.getError()
871+
: "Failed to set foreground session");
872+
}
873+
}));
874+
}
875+
876+
/**
877+
* Subscribes to all session lifecycle events.
878+
* <p>
879+
* Lifecycle events are emitted when sessions are created, deleted, updated, or
880+
* change foreground/background state (in TUI+server mode).
881+
*
882+
* @param handler
883+
* a callback that receives lifecycle events
884+
* @return an AutoCloseable that, when closed, unsubscribes the handler
885+
*/
886+
public AutoCloseable onLifecycle(com.github.copilot.sdk.json.SessionLifecycleHandler handler) {
887+
synchronized (lifecycleHandlersLock) {
888+
lifecycleHandlers.add(handler);
889+
}
890+
return () -> {
891+
synchronized (lifecycleHandlersLock) {
892+
lifecycleHandlers.remove(handler);
893+
}
894+
};
895+
}
896+
897+
/**
898+
* Subscribes to a specific session lifecycle event type.
899+
*
900+
* @param eventType
901+
* the event type to listen for (use
902+
* {@link com.github.copilot.sdk.json.SessionLifecycleEventTypes}
903+
* constants)
904+
* @param handler
905+
* a callback that receives events of the specified type
906+
* @return an AutoCloseable that, when closed, unsubscribes the handler
907+
*/
908+
public AutoCloseable onLifecycle(String eventType, com.github.copilot.sdk.json.SessionLifecycleHandler handler) {
909+
synchronized (lifecycleHandlersLock) {
910+
typedLifecycleHandlers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler);
911+
}
912+
return () -> {
913+
synchronized (lifecycleHandlersLock) {
914+
List<com.github.copilot.sdk.json.SessionLifecycleHandler> handlers = typedLifecycleHandlers
915+
.get(eventType);
916+
if (handlers != null) {
917+
handlers.remove(handler);
918+
}
919+
}
920+
};
921+
}
922+
923+
void dispatchLifecycleEvent(com.github.copilot.sdk.json.SessionLifecycleEvent event) {
924+
List<com.github.copilot.sdk.json.SessionLifecycleHandler> typed;
925+
List<com.github.copilot.sdk.json.SessionLifecycleHandler> wildcard;
926+
927+
synchronized (lifecycleHandlersLock) {
928+
List<com.github.copilot.sdk.json.SessionLifecycleHandler> handlers = typedLifecycleHandlers
929+
.get(event.getType());
930+
typed = handlers != null ? new ArrayList<>(handlers) : new ArrayList<>();
931+
wildcard = new ArrayList<>(lifecycleHandlers);
932+
}
933+
934+
for (com.github.copilot.sdk.json.SessionLifecycleHandler handler : typed) {
935+
try {
936+
handler.onLifecycleEvent(event);
937+
} catch (Exception e) {
938+
LOG.log(Level.WARNING, "Lifecycle handler error", e);
939+
}
940+
}
941+
942+
for (com.github.copilot.sdk.json.SessionLifecycleHandler handler : wildcard) {
943+
try {
944+
handler.onLifecycleEvent(event);
945+
} catch (Exception e) {
946+
LOG.log(Level.WARNING, "Lifecycle handler error", e);
947+
}
948+
}
949+
}
950+
808951
private CompletableFuture<Connection> ensureConnected() {
809952
if (connectionFuture == null && !options.isAutoStart()) {
810953
throw new IllegalStateException("Client not connected. Call start() first.");

0 commit comments

Comments
 (0)