Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ public partial class CopilotClient : IDisposable, IAsyncDisposable
private readonly int? _optionsPort;
private readonly string? _optionsHost;

/// <summary>
/// Occurs when a new session is created.
/// </summary>
/// <remarks>
/// Subscribe to this event to hook into session events globally.
/// The handler receives the newly created <see cref="CopilotSession"/> instance.
/// </remarks>
public event Action<CopilotSession>? SessionCreated;

/// <summary>
/// Occurs when a session is destroyed.
/// </summary>
/// <remarks>
/// Subscribe to this event to perform cleanup when sessions end.
/// The handler receives the session ID of the destroyed session.
/// </remarks>
public event Action<string>? SessionDestroyed;
Comment on lines +69 to +78
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new SessionCreated and SessionDestroyed events lack test coverage. Given that the repository has comprehensive automated tests for session lifecycle (SessionTests.cs) and client functionality (ClientTests.cs), consider adding tests to verify: (1) SessionCreated is fired when sessions are created/resumed, (2) SessionDestroyed is fired when sessions are disposed, (3) event handlers can subscribe and receive the correct parameters, and (4) exception handling for event handlers (if implemented).

Copilot uses AI. Check for mistakes.

/// <summary>
/// Creates a new instance of <see cref="CopilotClient"/>.
/// </summary>
Expand Down Expand Up @@ -362,6 +380,27 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
throw new InvalidOperationException($"Session {response.SessionId} already exists");
}

session.OnDisposed = (id) =>
{
_sessions.TryRemove(id, out _);
try
{
SessionDestroyed?.Invoke(id);
}
catch (Exception ex)
{
Debug.WriteLine($"Exception in SessionDestroyed event handler: {ex}");
}
};
try
{
SessionCreated?.Invoke(session);
}
catch (Exception ex)
{
Debug.WriteLine($"Exception in SessionCreated event handler: {ex}");
}

return session;
}

Expand Down Expand Up @@ -416,6 +455,28 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes

// 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 _);
try
{
SessionDestroyed?.Invoke(id);
}
catch (System.Exception ex)
{
Debug.WriteLine($"Exception in SessionDestroyed event handler: {ex}");
}
};
try
{
SessionCreated?.Invoke(session);
}
catch (System.Exception ex)
{
Debug.WriteLine($"Exception in SessionCreated event handler: {ex}");
}

return session;
}

Expand Down
15 changes: 15 additions & 0 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public partial class CopilotSession : IAsyncDisposable
/// </value>
public string? WorkspacePath { get; }

/// <summary>
/// Internal callback invoked when the session is disposed.
/// Used by CopilotClient to fire the SessionDestroyed event.
/// </summary>
internal Action<string>? OnDisposed { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="CopilotSession"/> class.
/// </summary>
Expand Down Expand Up @@ -431,6 +437,15 @@ await _rpc.InvokeWithCancellationAsync<object>(
{
_permissionHandlerLock.Release();
}

try
{
OnDisposed?.Invoke(SessionId);
}
catch (Exception)
{
// Swallow exceptions from disposal callbacks to avoid failing DisposeAsync.
}
}

private class OnDisposeCall(Action callback) : IDisposable
Expand Down