@@ -41,7 +41,7 @@ namespace GitHub.Copilot.SDK;
4141/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "gpt-4" });
4242///
4343/// // Handle events
44- /// using var subscription = session.On(evt =>
44+ /// using var subscription = session.On<SessionEvent> (evt =>
4545/// {
4646/// if (evt is AssistantMessageEvent assistantMessage)
4747/// Console.WriteLine(assistantMessage.Data?.Content);
@@ -82,11 +82,12 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
8282 private List < ModelInfo > ? _modelsCache ;
8383 private readonly SemaphoreSlim _modelsCacheLock = new ( 1 , 1 ) ;
8484 private readonly Func < CancellationToken , Task < IList < ModelInfo > > > ? _onListModels ;
85- private readonly List < Action < SessionLifecycleEvent > > _lifecycleHandlers = [ ] ;
86- private readonly Dictionary < string , List < Action < SessionLifecycleEvent > > > _typedLifecycleHandlers = [ ] ;
85+ private readonly List < LifecycleSubscription > _lifecycleHandlers = [ ] ;
8786 private readonly object _lifecycleHandlersLock = new ( ) ;
8887 private ServerRpc ? _serverRpc ;
8988
89+ private sealed record LifecycleSubscription ( Type EventType , Action < SessionLifecycleEvent > Handler ) ;
90+
9091 /// <summary>
9192 /// Gets the typed RPC client for server-scoped methods (no session required).
9293 /// </summary>
@@ -584,7 +585,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
584585 }
585586 if ( config . OnEvent != null )
586587 {
587- session . On ( config . OnEvent ) ;
588+ session . On < SessionEvent > ( config . OnEvent ) ;
588589 }
589590 ConfigureSessionFsHandlers ( session , config . CreateSessionFsProvider ) ;
590591 RegisterSession ( session ) ;
@@ -742,7 +743,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
742743 }
743744 if ( config . OnEvent != null )
744745 {
745- session . On ( config . OnEvent ) ;
746+ session . On < SessionEvent > ( config . OnEvent ) ;
746747 }
747748 ConfigureSessionFsHandlers ( session , config . CreateSessionFsProvider ) ;
748749 RegisterSession ( session ) ;
@@ -1102,103 +1103,59 @@ public async Task SetForegroundSessionIdAsync(string sessionId, CancellationToke
11021103 }
11031104
11041105 /// <summary>
1105- /// Subscribes to all session lifecycle events.
1106- /// </summary>
1107- /// <remarks>
1108- /// Lifecycle events are emitted when sessions are created, deleted, updated,
1109- /// or change foreground/background state (in TUI+server mode).
1110- /// </remarks>
1111- /// <param name="handler">A callback function that receives lifecycle events.</param>
1112- /// <returns>An IDisposable that, when disposed, unsubscribes the handler.</returns>
1113- /// <example>
1114- /// <code>
1115- /// using var subscription = client.On(evt =>
1116- /// {
1117- /// Console.WriteLine($"Session {evt.SessionId}: {evt.Type}");
1118- /// });
1119- /// </code>
1120- /// </example>
1121- public IDisposable On ( Action < SessionLifecycleEvent > handler )
1122- {
1123- ArgumentNullException . ThrowIfNull ( handler ) ;
1124-
1125- lock ( _lifecycleHandlersLock )
1126- {
1127- _lifecycleHandlers . Add ( handler ) ;
1128- }
1129-
1130- return new ActionDisposable ( ( ) =>
1131- {
1132- lock ( _lifecycleHandlersLock )
1133- {
1134- _lifecycleHandlers . Remove ( handler ) ;
1135- }
1136- } ) ;
1137- }
1138-
1139- /// <summary>
1140- /// Subscribes to a specific session lifecycle event type.
1106+ /// Subscribes to session lifecycle events of a specific kind.
11411107 /// </summary>
1142- /// <param name="eventType">The event type to listen for (use SessionLifecycleEventTypes constants).</param>
1143- /// <param name="handler">A callback function that receives events of the specified type.</param>
1144- /// <returns>An IDisposable that, when disposed, unsubscribes the handler.</returns>
1108+ /// <typeparam name="T">
1109+ /// The lifecycle event type to listen for. Pass a derived type such as
1110+ /// <see cref="SessionCreatedEvent"/> to filter by kind, or
1111+ /// <see cref="SessionLifecycleEvent"/> to receive every lifecycle event.
1112+ /// </typeparam>
1113+ /// <param name="handler">A callback invoked when a matching lifecycle event arrives.</param>
1114+ /// <returns>An <see cref="IDisposable"/> that, when disposed, unsubscribes the handler.</returns>
11451115 /// <example>
11461116 /// <code>
1147- /// using var subscription = client.On(SessionLifecycleEventTypes.Foreground, evt =>
1117+ /// using var sub = client.OnLifecycle<SessionForegroundEvent>( evt =>
11481118 /// {
11491119 /// Console.WriteLine($"Session {evt.SessionId} is now in foreground");
11501120 /// });
11511121 /// </code>
11521122 /// </example>
1153- public IDisposable On ( string eventType , Action < SessionLifecycleEvent > handler )
1123+ public IDisposable OnLifecycle < T > ( Action < T > handler ) where T : SessionLifecycleEvent
11541124 {
1155- ArgumentNullException . ThrowIfNull ( eventType ) ;
11561125 ArgumentNullException . ThrowIfNull ( handler ) ;
11571126
1127+ var subscription = new LifecycleSubscription ( typeof ( T ) , evt => handler ( ( T ) evt ) ) ;
1128+
11581129 lock ( _lifecycleHandlersLock )
11591130 {
1160- if ( ! _typedLifecycleHandlers . TryGetValue ( eventType , out var handlers ) )
1161- {
1162- handlers = [ ] ;
1163- _typedLifecycleHandlers [ eventType ] = handlers ;
1164- }
1165-
1166- handlers . Add ( handler ) ;
1131+ _lifecycleHandlers . Add ( subscription ) ;
11671132 }
11681133
11691134 return new ActionDisposable ( ( ) =>
11701135 {
11711136 lock ( _lifecycleHandlersLock )
11721137 {
1173- if ( _typedLifecycleHandlers . TryGetValue ( eventType , out var handlers ) )
1174- {
1175- handlers . Remove ( handler ) ;
1176- }
1138+ _lifecycleHandlers . Remove ( subscription ) ;
11771139 }
11781140 } ) ;
11791141 }
11801142
11811143 private void DispatchLifecycleEvent ( SessionLifecycleEvent evt )
11821144 {
1183- List < Action < SessionLifecycleEvent > > typedHandlers ;
1184- List < Action < SessionLifecycleEvent > > wildcardHandlers ;
1185-
1145+ List < LifecycleSubscription > snapshot ;
11861146 lock ( _lifecycleHandlersLock )
11871147 {
1188- typedHandlers = _typedLifecycleHandlers . TryGetValue ( evt . Type , out var handlers )
1189- ? [ .. handlers ]
1190- : [ ] ;
1191- wildcardHandlers = [ .. _lifecycleHandlers ] ;
1148+ snapshot = [ .. _lifecycleHandlers ] ;
11921149 }
11931150
1194- foreach ( var handler in typedHandlers )
1151+ var eventType = evt . GetType ( ) ;
1152+ foreach ( var subscription in snapshot )
11951153 {
1196- try { handler ( evt ) ; } catch { /* Ignore handler errors */ }
1197- }
1198-
1199- foreach ( var handler in wildcardHandlers )
1200- {
1201- try { handler ( evt ) ; } catch { /* Ignore handler errors */ }
1154+ if ( ! subscription . EventType . IsAssignableFrom ( eventType ) )
1155+ {
1156+ continue ;
1157+ }
1158+ try { subscription . Handler ( evt ) ; } catch { /* Ignore handler errors */ }
12021159 }
12031160 }
12041161
@@ -1766,13 +1723,19 @@ public void OnSessionEvent(string sessionId, JsonElement? @event)
17661723
17671724 public void OnSessionLifecycle ( string type , string sessionId , JsonElement ? metadata )
17681725 {
1769- var evt = new SessionLifecycleEvent
1726+ SessionLifecycleEvent evt = type switch
17701727 {
1771- Type = type ,
1772- SessionId = sessionId
1728+ "session.created" => new SessionCreatedEvent ( ) ,
1729+ "session.deleted" => new SessionDeletedEvent ( ) ,
1730+ "session.updated" => new SessionUpdatedEvent ( ) ,
1731+ "session.foreground" => new SessionForegroundEvent ( ) ,
1732+ "session.background" => new SessionBackgroundEvent ( ) ,
1733+ _ => new SessionLifecycleEvent ( )
17731734 } ;
17741735
1775- if ( metadata != null )
1736+ evt . Type = type ;
1737+ evt . SessionId = sessionId ;
1738+ if ( metadata is not null )
17761739 {
17771740 evt . Metadata = JsonSerializer . Deserialize (
17781741 metadata . Value . GetRawText ( ) ,
0 commit comments