@@ -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