diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index ef7982cb..b080bcb5 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -59,6 +59,24 @@ public partial class CopilotClient : IDisposable, IAsyncDisposable
private readonly int? _optionsPort;
private readonly string? _optionsHost;
+ ///
+ /// Occurs when a new session is created.
+ ///
+ ///
+ /// Subscribe to this event to hook into session events globally.
+ /// The handler receives the newly created instance.
+ ///
+ public event Action? SessionCreated;
+
+ ///
+ /// Occurs when a session is destroyed.
+ ///
+ ///
+ /// Subscribe to this event to perform cleanup when sessions end.
+ /// The handler receives the session ID of the destroyed session.
+ ///
+ public event Action? SessionDestroyed;
+
///
/// Creates a new instance of .
///
@@ -362,6 +380,13 @@ public async Task CreateSessionAsync(SessionConfig? config = nul
throw new InvalidOperationException($"Session {response.SessionId} already exists");
}
+ session.OnDisposed = (id) =>
+ {
+ _sessions.TryRemove(id, out _);
+ SessionDestroyed?.Invoke(id);
+ };
+ SessionCreated?.Invoke(session);
+
return session;
}
@@ -414,8 +439,23 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
session.RegisterPermissionHandler(config.OnPermissionRequest);
}
+ // Clear OnDisposed on the old session to prevent it from firing SessionDestroyed
+ // if it gets disposed after being replaced
+ if (_sessions.TryGetValue(response.SessionId, out var oldSession))
+ {
+ oldSession.OnDisposed = null;
+ }
+
// Replace any existing session entry to ensure new config (like permission handler) is used
_sessions[response.SessionId] = session;
+
+ session.OnDisposed = (id) =>
+ {
+ _sessions.TryRemove(id, out _);
+ SessionDestroyed?.Invoke(id);
+ };
+ SessionCreated?.Invoke(session);
+
return session;
}
diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs
index f1e47df8..dafd0923 100644
--- a/dotnet/src/Session.cs
+++ b/dotnet/src/Session.cs
@@ -64,6 +64,12 @@ public partial class CopilotSession : IAsyncDisposable
///
public string? WorkspacePath { get; }
+ ///
+ /// Internal callback invoked when the session is disposed.
+ /// Used by CopilotClient to fire the SessionDestroyed event.
+ ///
+ internal Action? OnDisposed { get; set; }
+
///
/// Initializes a new instance of the class.
///
@@ -431,6 +437,8 @@ await _rpc.InvokeWithCancellationAsync