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
6 changes: 5 additions & 1 deletion docs/guides/session-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,16 @@ session_id = create_session_id("alice", "code-review")
### Listing Active Sessions

```typescript
// List all sessions
const sessions = await client.listSessions();
console.log(`Found ${sessions.length} sessions`);

for (const session of sessions) {
console.log(`- ${session.sessionId} (created: ${session.createdAt})`);
}

// Filter sessions by repository
const repoSessions = await client.listSessions({ repository: "owner/repo" });
```

### Cleaning Up Old Sessions
Expand Down Expand Up @@ -521,7 +525,7 @@ await withSessionLock("user-123-task-456", async () => {
| **Create resumable session** | Provide your own `sessionId` |
| **Resume session** | `client.resumeSession(sessionId)` |
| **BYOK resume** | Re-provide `provider` config |
| **List sessions** | `client.listSessions()` |
| **List sessions** | `client.listSessions(filter?)` |
| **Delete session** | `client.deleteSession(sessionId)` |
| **Destroy active session** | `session.destroy()` |
| **Containerized deployment** | Mount `~/.copilot/session-state/` to persistent storage |
Expand Down
9 changes: 7 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell
/// <summary>
/// Lists all sessions known to the Copilot server.
/// </summary>
/// <param name="filter">Optional filter to narrow down the session list by cwd, git root, repository, or branch.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task that resolves with a list of <see cref="SessionMetadata"/> for all available sessions.</returns>
/// <exception cref="InvalidOperationException">Thrown when the client is not connected.</exception>
Expand All @@ -664,12 +665,12 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell
/// }
/// </code>
/// </example>
public async Task<List<SessionMetadata>> ListSessionsAsync(CancellationToken cancellationToken = default)
public async Task<List<SessionMetadata>> ListSessionsAsync(SessionListFilter? filter = null, CancellationToken cancellationToken = default)
{
var connection = await EnsureConnectedAsync(cancellationToken);

var response = await InvokeRpcAsync<ListSessionsResponse>(
connection.Rpc, "session.list", [], cancellationToken);
connection.Rpc, "session.list", [new ListSessionsRequest(filter)], cancellationToken);

return response.Sessions;
}
Expand Down Expand Up @@ -1369,6 +1370,9 @@ internal record DeleteSessionResponse(
bool Success,
string? Error);

internal record ListSessionsRequest(
SessionListFilter? Filter);

internal record ListSessionsResponse(
List<SessionMetadata> Sessions);

Expand Down Expand Up @@ -1438,6 +1442,7 @@ public override void WriteLine(string? message) =>
[JsonSerializable(typeof(DeleteSessionResponse))]
[JsonSerializable(typeof(GetLastSessionIdResponse))]
[JsonSerializable(typeof(HooksInvokeResponse))]
[JsonSerializable(typeof(ListSessionsRequest))]
[JsonSerializable(typeof(ListSessionsResponse))]
[JsonSerializable(typeof(PermissionRequestResponse))]
[JsonSerializable(typeof(PermissionRequestResult))]
Expand Down
33 changes: 33 additions & 0 deletions dotnet/src/Generated/SessionEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(SessionHandoffEvent), "session.handoff")]
[JsonDerivedType(typeof(SessionIdleEvent), "session.idle")]
[JsonDerivedType(typeof(SessionInfoEvent), "session.info")]
[JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")]
[JsonDerivedType(typeof(SessionModelChangeEvent), "session.model_change")]
[JsonDerivedType(typeof(SessionResumeEvent), "session.resume")]
[JsonDerivedType(typeof(SessionShutdownEvent), "session.shutdown")]
Expand Down Expand Up @@ -148,6 +149,18 @@ public partial class SessionInfoEvent : SessionEvent
public required SessionInfoData Data { get; set; }
}

/// <summary>
/// Event: session.context_changed
/// </summary>
public partial class SessionContextChangedEvent : SessionEvent
{
[JsonIgnore]
public override string Type => "session.context_changed";

[JsonPropertyName("data")]
public required SessionContextChangedData Data { get; set; }
}

/// <summary>
/// Event: session.model_change
/// </summary>
Expand Down Expand Up @@ -605,6 +618,24 @@ public partial class SessionInfoData
public required string Message { get; set; }
}

public partial class SessionContextChangedData
{
[JsonPropertyName("cwd")]
public required string Cwd { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("gitRoot")]
public string? GitRoot { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("repository")]
public string? Repository { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("branch")]
public string? Branch { get; set; }
}

public partial class SessionModelChangeData
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
Expand Down Expand Up @@ -1425,6 +1456,8 @@ public enum SystemMessageDataRole
[JsonSerializable(typeof(SessionIdleEvent))]
[JsonSerializable(typeof(SessionInfoData))]
[JsonSerializable(typeof(SessionInfoEvent))]
[JsonSerializable(typeof(SessionContextChangedData))]
[JsonSerializable(typeof(SessionContextChangedEvent))]
[JsonSerializable(typeof(SessionModelChangeData))]
[JsonSerializable(typeof(SessionModelChangeEvent))]
[JsonSerializable(typeof(SessionResumeData))]
Expand Down
34 changes: 34 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -881,13 +881,45 @@ public class MessageOptions

public delegate void SessionEventHandler(SessionEvent sessionEvent);

/// <summary>
/// Working directory context for a session.
/// </summary>
public class SessionContext
{
/// <summary>Working directory where the session was created.</summary>
public string Cwd { get; set; } = string.Empty;
/// <summary>Git repository root (if in a git repo).</summary>
public string? GitRoot { get; set; }
/// <summary>GitHub repository in "owner/repo" format.</summary>
public string? Repository { get; set; }
/// <summary>Current git branch.</summary>
public string? Branch { get; set; }
}

/// <summary>
/// Filter options for listing sessions.
/// </summary>
public class SessionListFilter
{
/// <summary>Filter by exact cwd match.</summary>
public string? Cwd { get; set; }
/// <summary>Filter by git root.</summary>
public string? GitRoot { get; set; }
/// <summary>Filter by repository (owner/repo format).</summary>
public string? Repository { get; set; }
/// <summary>Filter by branch.</summary>
public string? Branch { get; set; }
}

public class SessionMetadata
{
public string SessionId { get; set; } = string.Empty;
public DateTime StartTime { get; set; }
public DateTime ModifiedTime { get; set; }
public string? Summary { get; set; }
public bool IsRemote { get; set; }
/// <summary>Working directory context (cwd, git info) from session creation.</summary>
public SessionContext? Context { get; set; }
}

internal class PingRequest
Expand Down Expand Up @@ -1159,8 +1191,10 @@ public class SetForegroundSessionResponse
[JsonSerializable(typeof(PingRequest))]
[JsonSerializable(typeof(PingResponse))]
[JsonSerializable(typeof(ProviderConfig))]
[JsonSerializable(typeof(SessionContext))]
[JsonSerializable(typeof(SessionLifecycleEvent))]
[JsonSerializable(typeof(SessionLifecycleEventMetadata))]
[JsonSerializable(typeof(SessionListFilter))]
[JsonSerializable(typeof(SessionMetadata))]
[JsonSerializable(typeof(SetForegroundSessionResponse))]
[JsonSerializable(typeof(SystemMessageConfig))]
Expand Down
24 changes: 24 additions & 0 deletions dotnet/test/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,30 @@ public async Task SendAndWait_Blocks_Until_Session_Idle_And_Returns_Final_Assist
Assert.Contains("assistant.message", events);
}

[Fact]
public async Task Should_List_Sessions_With_Context()
{
var session = await Client.CreateSessionAsync();
await session.SendAndWaitAsync(new MessageOptions { Prompt = "Say hello" });

await Task.Delay(200);

var sessions = await Client.ListSessionsAsync();
Assert.NotEmpty(sessions);

var ourSession = sessions.Find(s => s.SessionId == session.SessionId);
Assert.NotNull(ourSession);

// Verify context field
foreach (var s in sessions)
{
if (s.Context != null)
{
Assert.False(string.IsNullOrEmpty(s.Context.Cwd), "Expected context.Cwd to be non-empty when context is present");
}
}
Comment on lines +387 to +393
}

[Fact]
public async Task SendAndWait_Throws_On_Timeout()
{
Expand Down
2 changes: 1 addition & 1 deletion go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func main() {
- `CreateSession(config *SessionConfig) (*Session, error)` - Create a new session
- `ResumeSession(sessionID string) (*Session, error)` - Resume an existing session
- `ResumeSessionWithOptions(sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume with additional configuration
- `ListSessions() ([]SessionMetadata, error)` - List all sessions known to the server
- `ListSessions(filter *SessionListFilter) ([]SessionMetadata, error)` - List sessions (with optional filter)
- `DeleteSession(sessionID string) error` - Delete a session permanently
- `GetState() ConnectionState` - Get connection state
- `Ping(message string) (*PingResponse, error)` - Ping the server
Expand Down
18 changes: 14 additions & 4 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,23 +609,33 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
// ListSessions returns metadata about all sessions known to the server.
//
// Returns a list of SessionMetadata for all available sessions, including their IDs,
// timestamps, and optional summaries.
// timestamps, optional summaries, and context information.
//
// An optional filter can be provided to filter sessions by cwd, git root, repository, or branch.
//
// Example:
//
// sessions, err := client.ListSessions(context.Background())
// sessions, err := client.ListSessions(context.Background(), nil)
// if err != nil {
// log.Fatal(err)
// }
// for _, session := range sessions {
// fmt.Printf("Session: %s\n", session.SessionID)
// }
func (c *Client) ListSessions(ctx context.Context) ([]SessionMetadata, error) {
//
// Example with filter:
//
// sessions, err := client.ListSessions(context.Background(), &SessionListFilter{Repository: "owner/repo"})
func (c *Client) ListSessions(ctx context.Context, filter *SessionListFilter) ([]SessionMetadata, error) {
if err := c.ensureConnected(); err != nil {
return nil, err
}

result, err := c.client.Request("session.list", listSessionsRequest{})
params := listSessionsRequest{}
if filter != nil {
params.Filter = filter
}
result, err := c.client.Request("session.list", params)
if err != nil {
return nil, err
}
Expand Down
4 changes: 4 additions & 0 deletions go/generated_session_events.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions go/internal/e2e/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ func TestSession(t *testing.T) {
time.Sleep(200 * time.Millisecond)

// List sessions and verify they're included
sessions, err := client.ListSessions(t.Context())
sessions, err := client.ListSessions(t.Context(), nil)
if err != nil {
t.Fatalf("Failed to list sessions: %v", err)
}
Expand Down Expand Up @@ -812,6 +812,15 @@ func TestSession(t *testing.T) {
}
// isRemote is a boolean, so it's always set
}

// Verify context field is present on sessions
for _, s := range sessions {
if s.Context != nil {
if s.Context.Cwd == "" {
t.Error("Expected context.Cwd to be non-empty when context is present")
}
}
}
})

t.Run("should delete session", func(t *testing.T) {
Expand All @@ -834,7 +843,7 @@ func TestSession(t *testing.T) {
time.Sleep(200 * time.Millisecond)

// Verify session exists in the list
sessions, err := client.ListSessions(t.Context())
sessions, err := client.ListSessions(t.Context(), nil)
if err != nil {
t.Fatalf("Failed to list sessions: %v", err)
}
Expand All @@ -855,7 +864,7 @@ func TestSession(t *testing.T) {
}

// Verify session no longer exists in the list
sessionsAfter, err := client.ListSessions(t.Context())
sessionsAfter, err := client.ListSessions(t.Context(), nil)
if err != nil {
t.Fatalf("Failed to list sessions after delete: %v", err)
}
Expand Down
39 changes: 33 additions & 6 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,13 +541,38 @@ type ModelInfo struct {
DefaultReasoningEffort string `json:"defaultReasoningEffort,omitempty"`
}

// SessionContext contains working directory context for a session
type SessionContext struct {
// Cwd is the working directory where the session was created
Cwd string `json:"cwd"`
// GitRoot is the git repository root (if in a git repo)
GitRoot string `json:"gitRoot,omitempty"`
// Repository is the GitHub repository in "owner/repo" format
Repository string `json:"repository,omitempty"`
// Branch is the current git branch
Branch string `json:"branch,omitempty"`
}

// SessionListFilter contains filter options for listing sessions
type SessionListFilter struct {
// Cwd filters by exact working directory match
Cwd string `json:"cwd,omitempty"`
// GitRoot filters by git root
GitRoot string `json:"gitRoot,omitempty"`
// Repository filters by repository (owner/repo format)
Repository string `json:"repository,omitempty"`
// Branch filters by branch
Branch string `json:"branch,omitempty"`
}

// SessionMetadata contains metadata about a session
type SessionMetadata struct {
SessionID string `json:"sessionId"`
StartTime string `json:"startTime"`
ModifiedTime string `json:"modifiedTime"`
Summary *string `json:"summary,omitempty"`
IsRemote bool `json:"isRemote"`
SessionID string `json:"sessionId"`
StartTime string `json:"startTime"`
ModifiedTime string `json:"modifiedTime"`
Summary *string `json:"summary,omitempty"`
IsRemote bool `json:"isRemote"`
Context *SessionContext `json:"context,omitempty"`
}

// SessionLifecycleEventType represents the type of session lifecycle event
Expand Down Expand Up @@ -655,7 +680,9 @@ type hooksInvokeRequest struct {
}

// listSessionsRequest is the request for session.list
type listSessionsRequest struct{}
type listSessionsRequest struct {
Filter *SessionListFilter `json:"filter,omitempty"`
}

// listSessionsResponse is the response from session.list
type listSessionsResponse struct {
Expand Down
Loading
Loading