Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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");
}
}
}

[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