diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md index d1fb39e6..527f5ecc 100644 --- a/docs/guides/session-persistence.md +++ b/docs/guides/session-persistence.md @@ -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 @@ -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 | diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 74f1c66f..b0b014eb 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -652,6 +652,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell /// /// Lists all sessions known to the Copilot server. /// + /// Optional filter to narrow down the session list by cwd, git root, repository, or branch. /// A that can be used to cancel the operation. /// A task that resolves with a list of for all available sessions. /// Thrown when the client is not connected. @@ -664,12 +665,12 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell /// } /// /// - public async Task> ListSessionsAsync(CancellationToken cancellationToken = default) + public async Task> ListSessionsAsync(SessionListFilter? filter = null, CancellationToken cancellationToken = default) { var connection = await EnsureConnectedAsync(cancellationToken); var response = await InvokeRpcAsync( - connection.Rpc, "session.list", [], cancellationToken); + connection.Rpc, "session.list", [new ListSessionsRequest(filter)], cancellationToken); return response.Sessions; } @@ -1369,6 +1370,9 @@ internal record DeleteSessionResponse( bool Success, string? Error); + internal record ListSessionsRequest( + SessionListFilter? Filter); + internal record ListSessionsResponse( List Sessions); @@ -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))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 02258839..05c71a5d 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -6,7 +6,7 @@ // // Generated from: @github/copilot/session-events.schema.json // Generated by: scripts/generate-session-types.ts -// Generated at: 2026-02-06T20:38:23.832Z +// Generated at: 2026-02-12T22:32:12.047Z // // To update these types: // 1. Update the schema in copilot-agent-runtime @@ -37,6 +37,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")] [JsonDerivedType(typeof(SessionCompactionCompleteEvent), "session.compaction_complete")] [JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")] +[JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")] [JsonDerivedType(typeof(SessionErrorEvent), "session.error")] [JsonDerivedType(typeof(SessionHandoffEvent), "session.handoff")] [JsonDerivedType(typeof(SessionIdleEvent), "session.idle")] @@ -46,8 +47,10 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(SessionShutdownEvent), "session.shutdown")] [JsonDerivedType(typeof(SessionSnapshotRewindEvent), "session.snapshot_rewind")] [JsonDerivedType(typeof(SessionStartEvent), "session.start")] +[JsonDerivedType(typeof(SessionTitleChangedEvent), "session.title_changed")] [JsonDerivedType(typeof(SessionTruncationEvent), "session.truncation")] [JsonDerivedType(typeof(SessionUsageInfoEvent), "session.usage_info")] +[JsonDerivedType(typeof(SessionWarningEvent), "session.warning")] [JsonDerivedType(typeof(SkillInvokedEvent), "skill.invoked")] [JsonDerivedType(typeof(SubagentCompletedEvent), "subagent.completed")] [JsonDerivedType(typeof(SubagentFailedEvent), "subagent.failed")] @@ -136,6 +139,18 @@ public partial class SessionIdleEvent : SessionEvent public required SessionIdleData Data { get; set; } } +/// +/// Event: session.title_changed +/// +public partial class SessionTitleChangedEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "session.title_changed"; + + [JsonPropertyName("data")] + public required SessionTitleChangedData Data { get; set; } +} + /// /// Event: session.info /// @@ -148,6 +163,18 @@ public partial class SessionInfoEvent : SessionEvent public required SessionInfoData Data { get; set; } } +/// +/// Event: session.warning +/// +public partial class SessionWarningEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "session.warning"; + + [JsonPropertyName("data")] + public required SessionWarningData Data { get; set; } +} + /// /// Event: session.model_change /// @@ -208,6 +235,18 @@ public partial class SessionShutdownEvent : SessionEvent public required SessionShutdownData Data { get; set; } } +/// +/// Event: session.context_changed +/// +public partial class SessionContextChangedEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "session.context_changed"; + + [JsonPropertyName("data")] + public required SessionContextChangedData Data { get; set; } +} + /// /// Event: session.usage_info /// @@ -596,6 +635,12 @@ public partial class SessionIdleData { } +public partial class SessionTitleChangedData +{ + [JsonPropertyName("title")] + public required string Title { get; set; } +} + public partial class SessionInfoData { [JsonPropertyName("infoType")] @@ -605,6 +650,15 @@ public partial class SessionInfoData public required string Message { get; set; } } +public partial class SessionWarningData +{ + [JsonPropertyName("warningType")] + public required string WarningType { get; set; } + + [JsonPropertyName("message")] + public required string Message { get; set; } +} + public partial class SessionModelChangeData { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -705,6 +759,24 @@ public partial class SessionShutdownData public string? CurrentModel { 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 SessionUsageInfoData { [JsonPropertyName("tokenLimit")] @@ -787,6 +859,10 @@ public partial class UserMessageData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] public string? Source { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentMode")] + public UserMessageDataAgentMode? AgentMode { get; set; } } public partial class PendingMessagesModifiedData @@ -847,6 +923,10 @@ public partial class AssistantMessageData [JsonPropertyName("encryptedContent")] public string? EncryptedContent { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } @@ -1054,6 +1134,9 @@ public partial class SubagentCompletedData [JsonPropertyName("agentName")] public required string AgentName { get; set; } + + [JsonPropertyName("agentDisplayName")] + public required string AgentDisplayName { get; set; } } public partial class SubagentFailedData @@ -1064,6 +1147,9 @@ public partial class SubagentFailedData [JsonPropertyName("agentName")] public required string AgentName { get; set; } + [JsonPropertyName("agentDisplayName")] + public required string AgentDisplayName { get; set; } + [JsonPropertyName("error")] public required string Error { get; set; } } @@ -1203,6 +1289,15 @@ public partial class SessionCompactionCompleteDataCompactionTokensUsed public required double CachedInput { get; set; } } +public partial class UserMessageDataAttachmentsItemFileLineRange +{ + [JsonPropertyName("start")] + public required double Start { get; set; } + + [JsonPropertyName("end")] + public required double End { get; set; } +} + public partial class UserMessageDataAttachmentsItemFile : UserMessageDataAttachmentsItem { [JsonIgnore] @@ -1213,6 +1308,19 @@ public partial class UserMessageDataAttachmentsItemFile : UserMessageDataAttachm [JsonPropertyName("displayName")] public required string DisplayName { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("lineRange")] + public UserMessageDataAttachmentsItemFileLineRange? LineRange { get; set; } +} + +public partial class UserMessageDataAttachmentsItemDirectoryLineRange +{ + [JsonPropertyName("start")] + public required double Start { get; set; } + + [JsonPropertyName("end")] + public required double End { get; set; } } public partial class UserMessageDataAttachmentsItemDirectory : UserMessageDataAttachmentsItem @@ -1225,6 +1333,10 @@ public partial class UserMessageDataAttachmentsItemDirectory : UserMessageDataAt [JsonPropertyName("displayName")] public required string DisplayName { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("lineRange")] + public UserMessageDataAttachmentsItemDirectoryLineRange? LineRange { get; set; } } public partial class UserMessageDataAttachmentsItemSelectionSelectionStart @@ -1302,6 +1414,131 @@ public partial class AssistantMessageDataToolRequestsItem public AssistantMessageDataToolRequestsItemType? Type { get; set; } } +public partial class ToolExecutionCompleteDataResultContentsItemText : ToolExecutionCompleteDataResultContentsItem +{ + [JsonIgnore] + public override string Type => "text"; + + [JsonPropertyName("text")] + public required string Text { get; set; } +} + +public partial class ToolExecutionCompleteDataResultContentsItemTerminal : ToolExecutionCompleteDataResultContentsItem +{ + [JsonIgnore] + public override string Type => "terminal"; + + [JsonPropertyName("text")] + public required string Text { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("exitCode")] + public double? ExitCode { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } +} + +public partial class ToolExecutionCompleteDataResultContentsItemImage : ToolExecutionCompleteDataResultContentsItem +{ + [JsonIgnore] + public override string Type => "image"; + + [JsonPropertyName("data")] + public required string Data { get; set; } + + [JsonPropertyName("mimeType")] + public required string MimeType { get; set; } +} + +public partial class ToolExecutionCompleteDataResultContentsItemAudio : ToolExecutionCompleteDataResultContentsItem +{ + [JsonIgnore] + public override string Type => "audio"; + + [JsonPropertyName("data")] + public required string Data { get; set; } + + [JsonPropertyName("mimeType")] + public required string MimeType { get; set; } +} + +public partial class ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem +{ + [JsonPropertyName("src")] + public required string Src { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sizes")] + public string[]? Sizes { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("theme")] + public ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItemTheme? Theme { get; set; } +} + +public partial class ToolExecutionCompleteDataResultContentsItemResourceLink : ToolExecutionCompleteDataResultContentsItem +{ + [JsonIgnore] + public override string Type => "resource_link"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("icons")] + public ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem[]? Icons { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("uri")] + public required string Uri { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("size")] + public double? Size { get; set; } +} + +public partial class ToolExecutionCompleteDataResultContentsItemResource : ToolExecutionCompleteDataResultContentsItem +{ + [JsonIgnore] + public override string Type => "resource"; + + [JsonPropertyName("resource")] + public required object Resource { get; set; } +} + +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ToolExecutionCompleteDataResultContentsItemText), "text")] +[JsonDerivedType(typeof(ToolExecutionCompleteDataResultContentsItemTerminal), "terminal")] +[JsonDerivedType(typeof(ToolExecutionCompleteDataResultContentsItemImage), "image")] +[JsonDerivedType(typeof(ToolExecutionCompleteDataResultContentsItemAudio), "audio")] +[JsonDerivedType(typeof(ToolExecutionCompleteDataResultContentsItemResourceLink), "resource_link")] +[JsonDerivedType(typeof(ToolExecutionCompleteDataResultContentsItemResource), "resource")] +public partial class ToolExecutionCompleteDataResultContentsItem +{ + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; +} + + public partial class ToolExecutionCompleteDataResult { [JsonPropertyName("content")] @@ -1310,6 +1547,10 @@ public partial class ToolExecutionCompleteDataResult [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("detailedContent")] public string? DetailedContent { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contents")] + public ToolExecutionCompleteDataResultContentsItem[]? Contents { get; set; } } public partial class ToolExecutionCompleteDataError @@ -1361,6 +1602,19 @@ public enum SessionShutdownDataShutdownType Error, } +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum UserMessageDataAgentMode +{ + [JsonStringEnumMemberName("interactive")] + Interactive, + [JsonStringEnumMemberName("plan")] + Plan, + [JsonStringEnumMemberName("autopilot")] + Autopilot, + [JsonStringEnumMemberName("shell")] + Shell, +} + [JsonConverter(typeof(JsonStringEnumConverter))] public enum AssistantMessageDataToolRequestsItemType { @@ -1370,6 +1624,15 @@ public enum AssistantMessageDataToolRequestsItemType Custom, } +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItemTheme +{ + [JsonStringEnumMemberName("light")] + Light, + [JsonStringEnumMemberName("dark")] + Dark, +} + [JsonConverter(typeof(JsonStringEnumConverter))] public enum SystemMessageDataRole { @@ -1415,6 +1678,8 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(SessionCompactionCompleteEvent))] [JsonSerializable(typeof(SessionCompactionStartData))] [JsonSerializable(typeof(SessionCompactionStartEvent))] +[JsonSerializable(typeof(SessionContextChangedData))] +[JsonSerializable(typeof(SessionContextChangedEvent))] [JsonSerializable(typeof(SessionErrorData))] [JsonSerializable(typeof(SessionErrorEvent))] [JsonSerializable(typeof(SessionEvent))] @@ -1438,10 +1703,14 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(SessionStartData))] [JsonSerializable(typeof(SessionStartDataContext))] [JsonSerializable(typeof(SessionStartEvent))] +[JsonSerializable(typeof(SessionTitleChangedData))] +[JsonSerializable(typeof(SessionTitleChangedEvent))] [JsonSerializable(typeof(SessionTruncationData))] [JsonSerializable(typeof(SessionTruncationEvent))] [JsonSerializable(typeof(SessionUsageInfoData))] [JsonSerializable(typeof(SessionUsageInfoEvent))] +[JsonSerializable(typeof(SessionWarningData))] +[JsonSerializable(typeof(SessionWarningEvent))] [JsonSerializable(typeof(SkillInvokedData))] [JsonSerializable(typeof(SkillInvokedEvent))] [JsonSerializable(typeof(SubagentCompletedData))] @@ -1458,6 +1727,14 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(ToolExecutionCompleteData))] [JsonSerializable(typeof(ToolExecutionCompleteDataError))] [JsonSerializable(typeof(ToolExecutionCompleteDataResult))] +[JsonSerializable(typeof(ToolExecutionCompleteDataResultContentsItem))] +[JsonSerializable(typeof(ToolExecutionCompleteDataResultContentsItemAudio))] +[JsonSerializable(typeof(ToolExecutionCompleteDataResultContentsItemImage))] +[JsonSerializable(typeof(ToolExecutionCompleteDataResultContentsItemResource))] +[JsonSerializable(typeof(ToolExecutionCompleteDataResultContentsItemResourceLink))] +[JsonSerializable(typeof(ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem))] +[JsonSerializable(typeof(ToolExecutionCompleteDataResultContentsItemTerminal))] +[JsonSerializable(typeof(ToolExecutionCompleteDataResultContentsItemText))] [JsonSerializable(typeof(ToolExecutionCompleteEvent))] [JsonSerializable(typeof(ToolExecutionPartialResultData))] [JsonSerializable(typeof(ToolExecutionPartialResultEvent))] @@ -1470,7 +1747,9 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(UserMessageData))] [JsonSerializable(typeof(UserMessageDataAttachmentsItem))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemDirectory))] +[JsonSerializable(typeof(UserMessageDataAttachmentsItemDirectoryLineRange))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemFile))] +[JsonSerializable(typeof(UserMessageDataAttachmentsItemFileLineRange))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemSelection))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemSelectionSelection))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemSelectionSelectionEnd))] diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 664b35d9..c5fa5fd3 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -881,6 +881,36 @@ public class MessageOptions public delegate void SessionEventHandler(SessionEvent sessionEvent); +/// +/// Working directory context for a session. +/// +public class SessionContext +{ + /// Working directory where the session was created. + public string Cwd { get; set; } = string.Empty; + /// Git repository root (if in a git repo). + public string? GitRoot { get; set; } + /// GitHub repository in "owner/repo" format. + public string? Repository { get; set; } + /// Current git branch. + public string? Branch { get; set; } +} + +/// +/// Filter options for listing sessions. +/// +public class SessionListFilter +{ + /// Filter by exact cwd match. + public string? Cwd { get; set; } + /// Filter by git root. + public string? GitRoot { get; set; } + /// Filter by repository (owner/repo format). + public string? Repository { get; set; } + /// Filter by branch. + public string? Branch { get; set; } +} + public class SessionMetadata { public string SessionId { get; set; } = string.Empty; @@ -888,6 +918,8 @@ public class SessionMetadata public DateTime ModifiedTime { get; set; } public string? Summary { get; set; } public bool IsRemote { get; set; } + /// Working directory context (cwd, git info) from session creation. + public SessionContext? Context { get; set; } } internal class PingRequest @@ -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))] diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 13b23522..920ee67d 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -369,6 +369,25 @@ public async Task SendAndWait_Blocks_Until_Session_Idle_And_Returns_Final_Assist Assert.Contains("assistant.message", events); } + // TODO: Re-enable once test harness CAPI proxy supports this test's session lifecycle + [Fact(Skip = "Needs test harness CAPI proxy support")] + public async Task Should_List_Sessions_With_Context() + { + var session = await Client.CreateSessionAsync(); + + var sessions = await Client.ListSessionsAsync(); + Assert.NotEmpty(sessions); + + var ourSession = sessions.Find(s => s.SessionId == session.SessionId); + Assert.NotNull(ourSession); + + // Context may be present on sessions that have been persisted with workspace.yaml + if (ourSession.Context != null) + { + Assert.False(string.IsNullOrEmpty(ourSession.Context.Cwd), "Expected context.Cwd to be non-empty when context is present"); + } + } + [Fact] public async Task SendAndWait_Throws_On_Timeout() { diff --git a/go/README.md b/go/README.md index 58207101..9f1ec393 100644 --- a/go/README.md +++ b/go/README.md @@ -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 diff --git a/go/client.go b/go/client.go index 319c6588..616808ea 100644 --- a/go/client.go +++ b/go/client.go @@ -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 } diff --git a/go/generated_session_events.go b/go/generated_session_events.go index ec4de9be..74e8b0ed 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -2,7 +2,7 @@ // // Generated from: @github/copilot/session-events.schema.json // Generated by: scripts/generate-session-types.ts -// Generated at: 2026-02-06T20:38:23.463Z +// Generated at: 2026-02-12T22:32:11.694Z // // To update these types: // 1. Update the schema in copilot-agent-runtime @@ -56,12 +56,14 @@ type Data struct { ProviderCallID *string `json:"providerCallId,omitempty"` Stack *string `json:"stack,omitempty"` StatusCode *int64 `json:"statusCode,omitempty"` + Title *string `json:"title,omitempty"` InfoType *string `json:"infoType,omitempty"` + WarningType *string `json:"warningType,omitempty"` NewModel *string `json:"newModel,omitempty"` PreviousModel *string `json:"previousModel,omitempty"` HandoffTime *time.Time `json:"handoffTime,omitempty"` RemoteSessionID *string `json:"remoteSessionId,omitempty"` - Repository *Repository `json:"repository,omitempty"` + Repository *RepositoryUnion `json:"repository"` SourceType *SourceType `json:"sourceType,omitempty"` Summary *string `json:"summary,omitempty"` MessagesRemovedDuringTruncation *float64 `json:"messagesRemovedDuringTruncation,omitempty"` @@ -82,6 +84,9 @@ type Data struct { ShutdownType *ShutdownType `json:"shutdownType,omitempty"` TotalAPIDurationMS *float64 `json:"totalApiDurationMs,omitempty"` TotalPremiumRequests *float64 `json:"totalPremiumRequests,omitempty"` + Branch *string `json:"branch,omitempty"` + Cwd *string `json:"cwd,omitempty"` + GitRoot *string `json:"gitRoot,omitempty"` CurrentTokens *float64 `json:"currentTokens,omitempty"` MessagesLength *float64 `json:"messagesLength,omitempty"` CheckpointNumber *float64 `json:"checkpointNumber,omitempty"` @@ -96,6 +101,7 @@ type Data struct { Success *bool `json:"success,omitempty"` SummaryContent *string `json:"summaryContent,omitempty"` TokensRemoved *float64 `json:"tokensRemoved,omitempty"` + AgentMode *AgentMode `json:"agentMode,omitempty"` Attachments []Attachment `json:"attachments,omitempty"` Content *string `json:"content,omitempty"` Source *string `json:"source,omitempty"` @@ -107,6 +113,7 @@ type Data struct { EncryptedContent *string `json:"encryptedContent,omitempty"` MessageID *string `json:"messageId,omitempty"` ParentToolCallID *string `json:"parentToolCallId,omitempty"` + Phase *string `json:"phase,omitempty"` ReasoningOpaque *string `json:"reasoningOpaque,omitempty"` ReasoningText *string `json:"reasoningText,omitempty"` ToolRequests []ToolRequest `json:"toolRequests,omitempty"` @@ -149,6 +156,7 @@ type Data struct { type Attachment struct { DisplayName string `json:"displayName"` + LineRange *LineRange `json:"lineRange,omitempty"` Path *string `json:"path,omitempty"` Type AttachmentType `json:"type"` FilePath *string `json:"filePath,omitempty"` @@ -156,6 +164,11 @@ type Attachment struct { Text *string `json:"text,omitempty"` } +type LineRange struct { + End float64 `json:"end"` + Start float64 `json:"start"` +} + type SelectionClass struct { End End `json:"end"` Start Start `json:"start"` @@ -229,15 +242,46 @@ type QuotaSnapshot struct { UsedRequests float64 `json:"usedRequests"` } -type Repository struct { +type RepositoryClass struct { Branch *string `json:"branch,omitempty"` Name string `json:"name"` Owner string `json:"owner"` } type Result struct { - Content string `json:"content"` - DetailedContent *string `json:"detailedContent,omitempty"` + Content string `json:"content"` + Contents []Content `json:"contents,omitempty"` + DetailedContent *string `json:"detailedContent,omitempty"` +} + +type Content struct { + Text *string `json:"text,omitempty"` + Type ContentType `json:"type"` + Cwd *string `json:"cwd,omitempty"` + ExitCode *float64 `json:"exitCode,omitempty"` + Data *string `json:"data,omitempty"` + MIMEType *string `json:"mimeType,omitempty"` + Description *string `json:"description,omitempty"` + Icons []Icon `json:"icons,omitempty"` + Name *string `json:"name,omitempty"` + Size *float64 `json:"size,omitempty"` + Title *string `json:"title,omitempty"` + URI *string `json:"uri,omitempty"` + Resource *ResourceClass `json:"resource,omitempty"` +} + +type Icon struct { + MIMEType *string `json:"mimeType,omitempty"` + Sizes []string `json:"sizes,omitempty"` + Src string `json:"src"` + Theme *Theme `json:"theme,omitempty"` +} + +type ResourceClass struct { + MIMEType *string `json:"mimeType,omitempty"` + Text *string `json:"text,omitempty"` + URI string `json:"uri"` + Blob *string `json:"blob,omitempty"` } type ToolRequest struct { @@ -247,6 +291,15 @@ type ToolRequest struct { Type *ToolRequestType `json:"type,omitempty"` } +type AgentMode string + +const ( + Autopilot AgentMode = "autopilot" + Interactive AgentMode = "interactive" + Plan AgentMode = "plan" + Shell AgentMode = "shell" +) + type AttachmentType string const ( @@ -255,6 +308,24 @@ const ( Selection AttachmentType = "selection" ) +type Theme string + +const ( + Dark Theme = "dark" + Light Theme = "light" +) + +type ContentType string + +const ( + Audio ContentType = "audio" + Image ContentType = "image" + Resource ContentType = "resource" + ResourceLink ContentType = "resource_link" + Terminal ContentType = "terminal" + Text ContentType = "text" +) + type Role string const ( @@ -300,6 +371,7 @@ const ( PendingMessagesModified SessionEventType = "pending_messages.modified" SessionCompactionComplete SessionEventType = "session.compaction_complete" SessionCompactionStart SessionEventType = "session.compaction_start" + SessionContextChanged SessionEventType = "session.context_changed" SessionError SessionEventType = "session.error" SessionHandoff SessionEventType = "session.handoff" SessionIdle SessionEventType = "session.idle" @@ -309,8 +381,10 @@ const ( SessionShutdown SessionEventType = "session.shutdown" SessionSnapshotRewind SessionEventType = "session.snapshot_rewind" SessionStart SessionEventType = "session.start" + SessionTitleChanged SessionEventType = "session.title_changed" SessionTruncation SessionEventType = "session.truncation" SessionUsageInfo SessionEventType = "session.usage_info" + SessionWarning SessionEventType = "session.warning" SkillInvoked SessionEventType = "skill.invoked" SubagentCompleted SessionEventType = "subagent.completed" SubagentFailed SessionEventType = "subagent.failed" @@ -369,6 +443,28 @@ func (x *ErrorUnion) MarshalJSON() ([]byte, error) { return marshalUnion(nil, nil, nil, x.String, false, nil, x.ErrorClass != nil, x.ErrorClass, false, nil, false, nil, false) } +type RepositoryUnion struct { + RepositoryClass *RepositoryClass + String *string +} + +func (x *RepositoryUnion) UnmarshalJSON(data []byte) error { + x.RepositoryClass = nil + var c RepositoryClass + object, err := unmarshalUnion(data, nil, nil, nil, &x.String, false, nil, true, &c, false, nil, false, nil, false) + if err != nil { + return err + } + if object { + x.RepositoryClass = &c + } + return nil +} + +func (x *RepositoryUnion) MarshalJSON() ([]byte, error) { + return marshalUnion(nil, nil, nil, x.String, false, nil, x.RepositoryClass != nil, x.RepositoryClass, false, nil, false, nil, false) +} + func unmarshalUnion(data []byte, pi **int64, pf **float64, pb **bool, ps **string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) (bool, error) { if pi != nil { *pi = nil diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index 62183286..6a98da60 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -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) } @@ -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) { @@ -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) } @@ -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) } diff --git a/go/types.go b/go/types.go index a3b38ee3..2f972450 100644 --- a/go/types.go +++ b/go/types.go @@ -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 @@ -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 { diff --git a/nodejs/README.md b/nodejs/README.md index 3a78f419..ed0d897c 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -108,9 +108,25 @@ Ping the server to check connectivity. Get current connection state. -##### `listSessions(): Promise` +##### `listSessions(filter?: SessionListFilter): Promise` -List all available sessions. +List all available sessions. Optionally filter by working directory context. + +**SessionMetadata:** + +- `sessionId: string` - Unique session identifier +- `startTime: Date` - When the session was created +- `modifiedTime: Date` - When the session was last modified +- `summary?: string` - Optional session summary +- `isRemote: boolean` - Whether the session is remote +- `context?: SessionContext` - Working directory context from session creation + +**SessionContext:** + +- `cwd: string` - Working directory where the session was created +- `gitRoot?: string` - Git repository root (if in a git repo) +- `repository?: string` - GitHub repository in "owner/repo" format +- `branch?: string` - Current git branch ##### `deleteSession(sessionId: string): Promise` diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 266d994e..fb3a5f91 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.405", + "@github/copilot": "^0.0.409", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -662,26 +662,26 @@ } }, "node_modules/@github/copilot": { - "version": "0.0.405", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.405.tgz", - "integrity": "sha512-zp0kGSkoKrO4MTWefAxU5w2VEc02QnhPY3FmVxOeduh6ayDIz2V368mXxs46ThremdMnMyZPL1k989BW4NpOVw==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.409.tgz", + "integrity": "sha512-rkYWOKjTSuGg99KsgmA0QAP4X2cpJzAYk6lZDlVxKPhuLP03wC5E+jLctrSLjpxhX32p9n13rm1+7Jun80a1hw==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "0.0.405", - "@github/copilot-darwin-x64": "0.0.405", - "@github/copilot-linux-arm64": "0.0.405", - "@github/copilot-linux-x64": "0.0.405", - "@github/copilot-win32-arm64": "0.0.405", - "@github/copilot-win32-x64": "0.0.405" + "@github/copilot-darwin-arm64": "0.0.409", + "@github/copilot-darwin-x64": "0.0.409", + "@github/copilot-linux-arm64": "0.0.409", + "@github/copilot-linux-x64": "0.0.409", + "@github/copilot-win32-arm64": "0.0.409", + "@github/copilot-win32-x64": "0.0.409" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "0.0.405", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.405.tgz", - "integrity": "sha512-RVFpU1cEMqjR0rLpwLwbIfT7XzqqVoQX99G6nsj+WrHu3TIeCgfffyd2YShd4QwZYsMRoTfKB+rirQ+0G5Uiig==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.409.tgz", + "integrity": "sha512-yjrrp++UNNvRoWsZ1+UioBqb3DEVxL5M5ePnMO5/Sf1sngxh0y5P9P6ePFZU4PVlM5BgC38DtrcauZaKf/oArQ==", "cpu": [ "arm64" ], @@ -695,9 +695,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "0.0.405", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.405.tgz", - "integrity": "sha512-Xj2FyPzpZlfqPTuMrXtPNEijSmm2ivHvyMWgy5Ijv7Slabxe+2s3WXDaokE3SQHodK6M0Yle2yrx9kxiwWA+qw==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.409.tgz", + "integrity": "sha512-EhLfY5DGU/BZmwjVcfnwKuJA7BxS9zdNCGeynUq7z/SI93ziastFqOddUX4D+ySz6yMrrXieN8cUKgzAlRCOJg==", "cpu": [ "x64" ], @@ -711,9 +711,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "0.0.405", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.405.tgz", - "integrity": "sha512-16Wiq8EYB6ghwqZdYytnNkcCN4sT3jyt9XkjfMxI5DDdjLuPc8wbj5VV5pw8S6lZvBL4eAwXGE3+fPqXKxH6GQ==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.409.tgz", + "integrity": "sha512-O7b/9LmBO8ljPqNngonx+v5d3cOs6HKvj2E9f5/Flb9Uw2lut7g6KGerfDYCMZUpvFCMDfbZSBJD3SDuJj1uPg==", "cpu": [ "arm64" ], @@ -727,9 +727,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "0.0.405", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.405.tgz", - "integrity": "sha512-HXpg7p235//pAuCvcL9m2EeIrL/K6OUEkFeHF3BFHzqUJR4a69gKLsxtUg0ZctypHqo2SehGCRAyVippTVlTyg==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.409.tgz", + "integrity": "sha512-zSfFqyPxNaBE5/ClrSjsKxhhTpJaVOqSJY0q87iV9fw6xwdzcJ1/FlZGKjE7W8YVb4tdJx+OBMjQCU8WYewF1A==", "cpu": [ "x64" ], @@ -743,9 +743,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "0.0.405", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.405.tgz", - "integrity": "sha512-4JCUMiRjP7zB3j1XpEtJq7b7cxTzuwDJ9o76jayAL8HL9NhqKZ6Ys6uxhDA6f/l0N2GVD1TEICxsnPgadz6srg==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.409.tgz", + "integrity": "sha512-VizZsdK7L3ym/OR4wahiFx+6hFtaOYN9qvsHmNSo8pb65AZ6ORdRnCPE7w9ZejMpdNEa6x6WqHfxDKJlF85zyA==", "cpu": [ "arm64" ], @@ -759,9 +759,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "0.0.405", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.405.tgz", - "integrity": "sha512-uHoJ9N8kZbTLbzgqBE1szHwLElv2f+P2OWlqmRSawQhwPl0s7u55dka7mZYvj2ZoNvIyb0OyShCO56OpmCcy/w==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.409.tgz", + "integrity": "sha512-c6dP3XRFk550PmH1Vxe7n/bStNSLnVGH5B+ErUKXk/SPqmZ59pyoa7H2USNdoC6Nav5tkwYYR1vwNZRy+iKvrA==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index b6e23f40..435f4300 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -40,7 +40,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.405", + "@github/copilot": "^0.0.409", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index af6260c9..af426766 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -37,6 +37,8 @@ import type { SessionLifecycleEvent, SessionLifecycleEventType, SessionLifecycleHandler, + SessionContext, + SessionListFilter, SessionMetadata, Tool, ToolCallRequestPayload, @@ -804,27 +806,24 @@ export class CopilotClient { } /** - * Lists all available sessions known to the server. + * List all available sessions. * - * Returns metadata about each session including ID, timestamps, and summary. - * - * @returns A promise that resolves with an array of session metadata - * @throws Error if the client is not connected + * @param filter - Optional filter to limit returned sessions by context fields * * @example - * ```typescript + * // List all sessions * const sessions = await client.listSessions(); - * for (const session of sessions) { - * console.log(`${session.sessionId}: ${session.summary}`); - * } - * ``` + * + * @example + * // List sessions for a specific repository + * const sessions = await client.listSessions({ repository: "owner/repo" }); */ - async listSessions(): Promise { + async listSessions(filter?: SessionListFilter): Promise { if (!this.connection) { throw new Error("Client not connected"); } - const response = await this.connection.sendRequest("session.list", {}); + const response = await this.connection.sendRequest("session.list", { filter }); const { sessions } = response as { sessions: Array<{ sessionId: string; @@ -832,6 +831,7 @@ export class CopilotClient { modifiedTime: string; summary?: string; isRemote: boolean; + context?: SessionContext; }>; }; @@ -841,6 +841,7 @@ export class CopilotClient { modifiedTime: new Date(s.modifiedTime), summary: s.summary, isRemote: s.isRemote, + context: s.context, })); } diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 86783a04..2d4af7c1 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -3,7 +3,7 @@ * * Generated from: @github/copilot/session-events.schema.json * Generated by: scripts/generate-session-types.ts - * Generated at: 2026-02-06T20:38:23.139Z + * Generated at: 2026-02-12T22:32:11.508Z * * To update these types: * 1. Update the schema in copilot-agent-runtime @@ -71,6 +71,16 @@ export type SessionEvent = type: "session.idle"; data: {}; } + | { + id: string; + timestamp: string; + parentId: string | null; + ephemeral: true; + type: "session.title_changed"; + data: { + title: string; + }; + } | { id: string; timestamp: string; @@ -82,6 +92,17 @@ export type SessionEvent = message: string; }; } + | { + id: string; + timestamp: string; + parentId: string | null; + ephemeral?: boolean; + type: "session.warning"; + data: { + warningType: string; + message: string; + }; + } | { id: string; timestamp: string; @@ -174,6 +195,19 @@ export type SessionEvent = currentModel?: string; }; } + | { + id: string; + timestamp: string; + parentId: string | null; + ephemeral?: boolean; + type: "session.context_changed"; + data: { + cwd: string; + gitRoot?: string; + repository?: string; + branch?: string; + }; + } | { id: string; timestamp: string; @@ -233,11 +267,19 @@ export type SessionEvent = type: "file"; path: string; displayName: string; + lineRange?: { + start: number; + end: number; + }; } | { type: "directory"; path: string; displayName: string; + lineRange?: { + start: number; + end: number; + }; } | { type: "selection"; @@ -257,6 +299,7 @@ export type SessionEvent = } )[]; source?: string; + agentMode?: "interactive" | "plan" | "autopilot" | "shell"; }; } | { @@ -327,6 +370,7 @@ export type SessionEvent = reasoningOpaque?: string; reasoningText?: string; encryptedContent?: string; + phase?: string; parentToolCallId?: string; }; } @@ -457,6 +501,57 @@ export type SessionEvent = result?: { content: string; detailedContent?: string; + contents?: ( + | { + type: "text"; + text: string; + } + | { + type: "terminal"; + text: string; + exitCode?: number; + cwd?: string; + } + | { + type: "image"; + data: string; + mimeType: string; + } + | { + type: "audio"; + data: string; + mimeType: string; + } + | { + icons?: { + src: string; + mimeType?: string; + sizes?: string[]; + theme?: "light" | "dark"; + }[]; + name: string; + title?: string; + uri: string; + description?: string; + mimeType?: string; + size?: number; + type: "resource_link"; + } + | { + type: "resource"; + resource: + | { + uri: string; + mimeType?: string; + text: string; + } + | { + uri: string; + mimeType?: string; + blob: string; + }; + } + )[]; }; error?: { message: string; @@ -503,6 +598,7 @@ export type SessionEvent = data: { toolCallId: string; agentName: string; + agentDisplayName: string; }; } | { @@ -514,6 +610,7 @@ export type SessionEvent = data: { toolCallId: string; agentName: string; + agentDisplayName: string; error: string; }; } diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 4f9fcbf6..5e73a1bb 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -39,6 +39,8 @@ export type { SessionLifecycleEvent, SessionLifecycleEventType, SessionLifecycleHandler, + SessionContext, + SessionListFilter, SessionMetadata, SystemMessageAppendConfig, SystemMessageConfig, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index ffb96801..c2806804 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -869,6 +869,34 @@ export type SessionEventHandler = (event: SessionEvent) => void; */ export type ConnectionState = "disconnected" | "connecting" | "connected" | "error"; +/** + * Working directory context for a session + */ +export interface SessionContext { + /** Working directory where the session was created */ + cwd: string; + /** Git repository root (if in a git repo) */ + gitRoot?: string; + /** GitHub repository in "owner/repo" format */ + repository?: string; + /** Current git branch */ + branch?: string; +} + +/** + * Filter options for listing sessions + */ +export interface SessionListFilter { + /** Filter by exact cwd match */ + cwd?: string; + /** Filter by git root */ + gitRoot?: string; + /** Filter by repository (owner/repo format) */ + repository?: string; + /** Filter by branch */ + branch?: string; +} + /** * Metadata about a session */ @@ -878,6 +906,8 @@ export interface SessionMetadata { modifiedTime: Date; summary?: string; isRemote: boolean; + /** Working directory context (cwd, git info) from session creation */ + context?: SessionContext; } /** diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index 01a3ad0b..de1e9e6d 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -22,6 +22,27 @@ describe("Sessions", async () => { await expect(() => session.getMessages()).rejects.toThrow(/Session not found/); }); + // TODO: Re-enable once test harness CAPI proxy supports this test's session lifecycle + it.skip("should list sessions with context field", { timeout: 60000 }, async () => { + // Create a session — just creating it is enough for it to appear in listSessions + const session = await client.createSession(); + expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); + + // Verify it has a start event (confirms session is active) + const messages = await session.getMessages(); + expect(messages.length).toBeGreaterThan(0); + + // List sessions and find the one we just created + const sessions = await client.listSessions(); + const ourSession = sessions.find((s) => s.sessionId === session.sessionId); + + expect(ourSession).toBeDefined(); + // Context may not be populated if workspace.yaml hasn't been written yet + if (ourSession?.context) { + expect(ourSession.context.cwd).toMatch(/^(\/|[A-Za-z]:)/); + } + }); + it("should have stateful conversation", async () => { const session = await client.createSession(); const assistantMessage = await session.sendAndWait({ prompt: "What is 1+1?" }); diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 90a05563..f5f7ed0b 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -28,7 +28,9 @@ ProviderConfig, ResumeSessionConfig, SessionConfig, + SessionContext, SessionEvent, + SessionListFilter, SessionMetadata, StopError, Tool, @@ -62,7 +64,9 @@ "ProviderConfig", "ResumeSessionConfig", "SessionConfig", + "SessionContext", "SessionEvent", + "SessionListFilter", "SessionMetadata", "StopError", "Tool", diff --git a/python/copilot/client.py b/python/copilot/client.py index 85b72897..e4c9104c 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -41,6 +41,7 @@ SessionLifecycleEvent, SessionLifecycleEventType, SessionLifecycleHandler, + SessionListFilter, SessionMetadata, StopError, ToolHandler, @@ -837,12 +838,18 @@ async def list_models(self) -> list["ModelInfo"]: return list(models) # Return a copy to prevent cache mutation - async def list_sessions(self) -> list["SessionMetadata"]: + async def list_sessions( + self, filter: "SessionListFilter | None" = None + ) -> list["SessionMetadata"]: """ List all available sessions known to the server. Returns metadata about each session including ID, timestamps, and summary. + Args: + filter: Optional filter to narrow down the list of sessions by cwd, git root, + repository, or branch. + Returns: A list of SessionMetadata objects. @@ -853,11 +860,18 @@ async def list_sessions(self) -> list["SessionMetadata"]: >>> sessions = await client.list_sessions() >>> for session in sessions: ... print(f"Session: {session.sessionId}") + >>> # Filter sessions by repository + >>> from copilot import SessionListFilter + >>> filtered = await client.list_sessions(SessionListFilter(repository="owner/repo")) """ if not self._client: raise RuntimeError("Client not connected") - response = await self._client.request("session.list", {}) + payload: dict = {} + if filter is not None: + payload["filter"] = filter.to_dict() + + response = await self._client.request("session.list", payload) sessions_data = response.get("sessions", []) return [SessionMetadata.from_dict(session) for session in sessions_data] diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 84dff82e..0621daa6 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -3,16 +3,16 @@ Generated from: @github/copilot/session-events.schema.json Generated by: scripts/generate-session-types.ts -Generated at: 2026-02-06T20:38:23.376Z +Generated at: 2026-02-12T22:32:11.650Z To update these types: 1. Update the schema in copilot-agent-runtime 2. Run: npm run generate:session-types """ +from enum import Enum from dataclasses import dataclass from typing import Any, Optional, List, Dict, Union, TypeVar, Type, cast, Callable -from enum import Enum from datetime import datetime from uuid import UUID import dateutil.parser @@ -85,6 +85,32 @@ def from_int(x: Any) -> int: return x +class AgentMode(Enum): + AUTOPILOT = "autopilot" + INTERACTIVE = "interactive" + PLAN = "plan" + SHELL = "shell" + + +@dataclass +class LineRange: + end: float + start: float + + @staticmethod + def from_dict(obj: Any) -> 'LineRange': + assert isinstance(obj, dict) + end = from_float(obj.get("end")) + start = from_float(obj.get("start")) + return LineRange(end, start) + + def to_dict(self) -> dict: + result: dict = {} + result["end"] = to_float(self.end) + result["start"] = to_float(self.start) + return result + + @dataclass class End: character: float @@ -152,6 +178,7 @@ class AttachmentType(Enum): class Attachment: display_name: str type: AttachmentType + line_range: Optional[LineRange] = None path: Optional[str] = None file_path: Optional[str] = None selection: Optional[Selection] = None @@ -162,16 +189,19 @@ def from_dict(obj: Any) -> 'Attachment': assert isinstance(obj, dict) display_name = from_str(obj.get("displayName")) type = AttachmentType(obj.get("type")) + line_range = from_union([LineRange.from_dict, from_none], obj.get("lineRange")) path = from_union([from_str, from_none], obj.get("path")) file_path = from_union([from_str, from_none], obj.get("filePath")) selection = from_union([Selection.from_dict, from_none], obj.get("selection")) text = from_union([from_str, from_none], obj.get("text")) - return Attachment(display_name, type, path, file_path, selection, text) + return Attachment(display_name, type, line_range, path, file_path, selection, text) def to_dict(self) -> dict: result: dict = {} result["displayName"] = from_str(self.display_name) result["type"] = to_enum(AttachmentType, self.type) + if self.line_range is not None: + result["lineRange"] = from_union([lambda x: to_class(LineRange, x), from_none], self.line_range) if self.path is not None: result["path"] = from_union([from_str, from_none], self.path) if self.file_path is not None: @@ -402,18 +432,18 @@ def to_dict(self) -> dict: @dataclass -class Repository: +class RepositoryClass: name: str owner: str branch: Optional[str] = None @staticmethod - def from_dict(obj: Any) -> 'Repository': + def from_dict(obj: Any) -> 'RepositoryClass': assert isinstance(obj, dict) name = from_str(obj.get("name")) owner = from_str(obj.get("owner")) branch = from_union([from_str, from_none], obj.get("branch")) - return Repository(name, owner, branch) + return RepositoryClass(name, owner, branch) def to_dict(self) -> dict: result: dict = {} @@ -424,21 +454,159 @@ def to_dict(self) -> dict: return result +class Theme(Enum): + DARK = "dark" + LIGHT = "light" + + +@dataclass +class Icon: + src: str + mime_type: Optional[str] = None + sizes: Optional[List[str]] = None + theme: Optional[Theme] = None + + @staticmethod + def from_dict(obj: Any) -> 'Icon': + assert isinstance(obj, dict) + src = from_str(obj.get("src")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + sizes = from_union([lambda x: from_list(from_str, x), from_none], obj.get("sizes")) + theme = from_union([Theme, from_none], obj.get("theme")) + return Icon(src, mime_type, sizes, theme) + + def to_dict(self) -> dict: + result: dict = {} + result["src"] = from_str(self.src) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.sizes is not None: + result["sizes"] = from_union([lambda x: from_list(from_str, x), from_none], self.sizes) + if self.theme is not None: + result["theme"] = from_union([lambda x: to_enum(Theme, x), from_none], self.theme) + return result + + +@dataclass +class Resource: + uri: str + mime_type: Optional[str] = None + text: Optional[str] = None + blob: Optional[str] = None + + @staticmethod + def from_dict(obj: Any) -> 'Resource': + assert isinstance(obj, dict) + uri = from_str(obj.get("uri")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + text = from_union([from_str, from_none], obj.get("text")) + blob = from_union([from_str, from_none], obj.get("blob")) + return Resource(uri, mime_type, text, blob) + + def to_dict(self) -> dict: + result: dict = {} + result["uri"] = from_str(self.uri) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.text is not None: + result["text"] = from_union([from_str, from_none], self.text) + if self.blob is not None: + result["blob"] = from_union([from_str, from_none], self.blob) + return result + + +class ContentType(Enum): + AUDIO = "audio" + IMAGE = "image" + RESOURCE = "resource" + RESOURCE_LINK = "resource_link" + TERMINAL = "terminal" + TEXT = "text" + + +@dataclass +class Content: + type: ContentType + text: Optional[str] = None + cwd: Optional[str] = None + exit_code: Optional[float] = None + data: Optional[str] = None + mime_type: Optional[str] = None + description: Optional[str] = None + icons: Optional[List[Icon]] = None + name: Optional[str] = None + size: Optional[float] = None + title: Optional[str] = None + uri: Optional[str] = None + resource: Optional[Resource] = None + + @staticmethod + def from_dict(obj: Any) -> 'Content': + assert isinstance(obj, dict) + type = ContentType(obj.get("type")) + text = from_union([from_str, from_none], obj.get("text")) + cwd = from_union([from_str, from_none], obj.get("cwd")) + exit_code = from_union([from_float, from_none], obj.get("exitCode")) + data = from_union([from_str, from_none], obj.get("data")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + description = from_union([from_str, from_none], obj.get("description")) + icons = from_union([lambda x: from_list(Icon.from_dict, x), from_none], obj.get("icons")) + name = from_union([from_str, from_none], obj.get("name")) + size = from_union([from_float, from_none], obj.get("size")) + title = from_union([from_str, from_none], obj.get("title")) + uri = from_union([from_str, from_none], obj.get("uri")) + resource = from_union([Resource.from_dict, from_none], obj.get("resource")) + return Content(type, text, cwd, exit_code, data, mime_type, description, icons, name, size, title, uri, resource) + + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(ContentType, self.type) + if self.text is not None: + result["text"] = from_union([from_str, from_none], self.text) + if self.cwd is not None: + result["cwd"] = from_union([from_str, from_none], self.cwd) + if self.exit_code is not None: + result["exitCode"] = from_union([to_float, from_none], self.exit_code) + if self.data is not None: + result["data"] = from_union([from_str, from_none], self.data) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.icons is not None: + result["icons"] = from_union([lambda x: from_list(lambda x: to_class(Icon, x), x), from_none], self.icons) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) + if self.size is not None: + result["size"] = from_union([to_float, from_none], self.size) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) + if self.uri is not None: + result["uri"] = from_union([from_str, from_none], self.uri) + if self.resource is not None: + result["resource"] = from_union([lambda x: to_class(Resource, x), from_none], self.resource) + return result + + @dataclass class Result: content: str + contents: Optional[List[Content]] = None detailed_content: Optional[str] = None @staticmethod def from_dict(obj: Any) -> 'Result': assert isinstance(obj, dict) content = from_str(obj.get("content")) + contents = from_union([lambda x: from_list(Content.from_dict, x), from_none], obj.get("contents")) detailed_content = from_union([from_str, from_none], obj.get("detailedContent")) - return Result(content, detailed_content) + return Result(content, contents, detailed_content) def to_dict(self) -> dict: result: dict = {} result["content"] = from_str(self.content) + if self.contents is not None: + result["contents"] = from_union([lambda x: from_list(lambda x: to_class(Content, x), x), from_none], self.contents) if self.detailed_content is not None: result["detailedContent"] = from_union([from_str, from_none], self.detailed_content) return result @@ -507,12 +675,14 @@ class Data: provider_call_id: Optional[str] = None stack: Optional[str] = None status_code: Optional[int] = None + title: Optional[str] = None info_type: Optional[str] = None + warning_type: Optional[str] = None new_model: Optional[str] = None previous_model: Optional[str] = None handoff_time: Optional[datetime] = None remote_session_id: Optional[str] = None - repository: Optional[Repository] = None + repository: Optional[Union[RepositoryClass, str]] = None source_type: Optional[SourceType] = None summary: Optional[str] = None messages_removed_during_truncation: Optional[float] = None @@ -533,6 +703,9 @@ class Data: shutdown_type: Optional[ShutdownType] = None total_api_duration_ms: Optional[float] = None total_premium_requests: Optional[float] = None + branch: Optional[str] = None + cwd: Optional[str] = None + git_root: Optional[str] = None current_tokens: Optional[float] = None messages_length: Optional[float] = None checkpoint_number: Optional[float] = None @@ -547,6 +720,7 @@ class Data: success: Optional[bool] = None summary_content: Optional[str] = None tokens_removed: Optional[float] = None + agent_mode: Optional[AgentMode] = None attachments: Optional[List[Attachment]] = None content: Optional[str] = None source: Optional[str] = None @@ -558,6 +732,7 @@ class Data: encrypted_content: Optional[str] = None message_id: Optional[str] = None parent_tool_call_id: Optional[str] = None + phase: Optional[str] = None reasoning_opaque: Optional[str] = None reasoning_text: Optional[str] = None tool_requests: Optional[List[ToolRequest]] = None @@ -614,12 +789,14 @@ def from_dict(obj: Any) -> 'Data': provider_call_id = from_union([from_str, from_none], obj.get("providerCallId")) stack = from_union([from_str, from_none], obj.get("stack")) status_code = from_union([from_int, from_none], obj.get("statusCode")) + title = from_union([from_str, from_none], obj.get("title")) info_type = from_union([from_str, from_none], obj.get("infoType")) + warning_type = from_union([from_str, from_none], obj.get("warningType")) new_model = from_union([from_str, from_none], obj.get("newModel")) previous_model = from_union([from_str, from_none], obj.get("previousModel")) handoff_time = from_union([from_datetime, from_none], obj.get("handoffTime")) remote_session_id = from_union([from_str, from_none], obj.get("remoteSessionId")) - repository = from_union([Repository.from_dict, from_none], obj.get("repository")) + repository = from_union([RepositoryClass.from_dict, from_str, from_none], obj.get("repository")) source_type = from_union([SourceType, from_none], obj.get("sourceType")) summary = from_union([from_str, from_none], obj.get("summary")) messages_removed_during_truncation = from_union([from_float, from_none], obj.get("messagesRemovedDuringTruncation")) @@ -640,6 +817,9 @@ def from_dict(obj: Any) -> 'Data': shutdown_type = from_union([ShutdownType, from_none], obj.get("shutdownType")) total_api_duration_ms = from_union([from_float, from_none], obj.get("totalApiDurationMs")) total_premium_requests = from_union([from_float, from_none], obj.get("totalPremiumRequests")) + branch = from_union([from_str, from_none], obj.get("branch")) + cwd = from_union([from_str, from_none], obj.get("cwd")) + git_root = from_union([from_str, from_none], obj.get("gitRoot")) current_tokens = from_union([from_float, from_none], obj.get("currentTokens")) messages_length = from_union([from_float, from_none], obj.get("messagesLength")) checkpoint_number = from_union([from_float, from_none], obj.get("checkpointNumber")) @@ -654,6 +834,7 @@ def from_dict(obj: Any) -> 'Data': success = from_union([from_bool, from_none], obj.get("success")) summary_content = from_union([from_str, from_none], obj.get("summaryContent")) tokens_removed = from_union([from_float, from_none], obj.get("tokensRemoved")) + agent_mode = from_union([AgentMode, from_none], obj.get("agentMode")) attachments = from_union([lambda x: from_list(Attachment.from_dict, x), from_none], obj.get("attachments")) content = from_union([from_str, from_none], obj.get("content")) source = from_union([from_str, from_none], obj.get("source")) @@ -665,6 +846,7 @@ def from_dict(obj: Any) -> 'Data': encrypted_content = from_union([from_str, from_none], obj.get("encryptedContent")) message_id = from_union([from_str, from_none], obj.get("messageId")) parent_tool_call_id = from_union([from_str, from_none], obj.get("parentToolCallId")) + phase = from_union([from_str, from_none], obj.get("phase")) reasoning_opaque = from_union([from_str, from_none], obj.get("reasoningOpaque")) reasoning_text = from_union([from_str, from_none], obj.get("reasoningText")) tool_requests = from_union([lambda x: from_list(ToolRequest.from_dict, x), from_none], obj.get("toolRequests")) @@ -703,7 +885,7 @@ def from_dict(obj: Any) -> 'Data': output = obj.get("output") metadata = from_union([Metadata.from_dict, from_none], obj.get("metadata")) role = from_union([Role, from_none], obj.get("role")) - return Data(context, copilot_version, producer, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, info_type, new_model, previous_model, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, attachments, content, source, transformed_content, turn_id, intent, reasoning_id, delta_content, encrypted_content, message_id, parent_tool_call_id, reasoning_opaque, reasoning_text, tool_requests, total_response_size_bytes, api_call_id, cache_read_tokens, cache_write_tokens, cost, duration, initiator, input_tokens, model, output_tokens, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, path, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role) + return Data(context, copilot_version, producer, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, title, info_type, warning_type, new_model, previous_model, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, branch, cwd, git_root, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, source, transformed_content, turn_id, intent, reasoning_id, delta_content, encrypted_content, message_id, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, total_response_size_bytes, api_call_id, cache_read_tokens, cache_write_tokens, cost, duration, initiator, input_tokens, model, output_tokens, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, path, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role) def to_dict(self) -> dict: result: dict = {} @@ -735,8 +917,12 @@ def to_dict(self) -> dict: result["stack"] = from_union([from_str, from_none], self.stack) if self.status_code is not None: result["statusCode"] = from_union([from_int, from_none], self.status_code) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) if self.info_type is not None: result["infoType"] = from_union([from_str, from_none], self.info_type) + if self.warning_type is not None: + result["warningType"] = from_union([from_str, from_none], self.warning_type) if self.new_model is not None: result["newModel"] = from_union([from_str, from_none], self.new_model) if self.previous_model is not None: @@ -746,7 +932,7 @@ def to_dict(self) -> dict: if self.remote_session_id is not None: result["remoteSessionId"] = from_union([from_str, from_none], self.remote_session_id) if self.repository is not None: - result["repository"] = from_union([lambda x: to_class(Repository, x), from_none], self.repository) + result["repository"] = from_union([lambda x: to_class(RepositoryClass, x), from_str, from_none], self.repository) if self.source_type is not None: result["sourceType"] = from_union([lambda x: to_enum(SourceType, x), from_none], self.source_type) if self.summary is not None: @@ -787,6 +973,12 @@ def to_dict(self) -> dict: result["totalApiDurationMs"] = from_union([to_float, from_none], self.total_api_duration_ms) if self.total_premium_requests is not None: result["totalPremiumRequests"] = from_union([to_float, from_none], self.total_premium_requests) + if self.branch is not None: + result["branch"] = from_union([from_str, from_none], self.branch) + if self.cwd is not None: + result["cwd"] = from_union([from_str, from_none], self.cwd) + if self.git_root is not None: + result["gitRoot"] = from_union([from_str, from_none], self.git_root) if self.current_tokens is not None: result["currentTokens"] = from_union([to_float, from_none], self.current_tokens) if self.messages_length is not None: @@ -815,6 +1007,8 @@ def to_dict(self) -> dict: result["summaryContent"] = from_union([from_str, from_none], self.summary_content) if self.tokens_removed is not None: result["tokensRemoved"] = from_union([to_float, from_none], self.tokens_removed) + if self.agent_mode is not None: + result["agentMode"] = from_union([lambda x: to_enum(AgentMode, x), from_none], self.agent_mode) if self.attachments is not None: result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(Attachment, x), x), from_none], self.attachments) if self.content is not None: @@ -837,6 +1031,8 @@ def to_dict(self) -> dict: result["messageId"] = from_union([from_str, from_none], self.message_id) if self.parent_tool_call_id is not None: result["parentToolCallId"] = from_union([from_str, from_none], self.parent_tool_call_id) + if self.phase is not None: + result["phase"] = from_union([from_str, from_none], self.phase) if self.reasoning_opaque is not None: result["reasoningOpaque"] = from_union([from_str, from_none], self.reasoning_opaque) if self.reasoning_text is not None: @@ -931,6 +1127,7 @@ class SessionEventType(Enum): PENDING_MESSAGES_MODIFIED = "pending_messages.modified" SESSION_COMPACTION_COMPLETE = "session.compaction_complete" SESSION_COMPACTION_START = "session.compaction_start" + SESSION_CONTEXT_CHANGED = "session.context_changed" SESSION_ERROR = "session.error" SESSION_HANDOFF = "session.handoff" SESSION_IDLE = "session.idle" @@ -940,8 +1137,10 @@ class SessionEventType(Enum): SESSION_SHUTDOWN = "session.shutdown" SESSION_SNAPSHOT_REWIND = "session.snapshot_rewind" SESSION_START = "session.start" + SESSION_TITLE_CHANGED = "session.title_changed" SESSION_TRUNCATION = "session.truncation" SESSION_USAGE_INFO = "session.usage_info" + SESSION_WARNING = "session.warning" SKILL_INVOKED = "skill.invoked" SUBAGENT_COMPLETED = "subagent.completed" SUBAGENT_FAILED = "subagent.failed" diff --git a/python/copilot/types.py b/python/copilot/types.py index 3cecbe64..b77e36be 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -918,6 +918,61 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionContext: + """Working directory context for a session""" + + cwd: str # Working directory where the session was created + gitRoot: str | None = None # Git repository root (if in a git repo) + repository: str | None = None # GitHub repository in "owner/repo" format + branch: str | None = None # Current git branch + + @staticmethod + def from_dict(obj: Any) -> SessionContext: + assert isinstance(obj, dict) + cwd = obj.get("cwd") + if cwd is None: + raise ValueError("Missing required field 'cwd' in SessionContext") + return SessionContext( + cwd=str(cwd), + gitRoot=obj.get("gitRoot"), + repository=obj.get("repository"), + branch=obj.get("branch"), + ) + + def to_dict(self) -> dict: + result: dict = {"cwd": self.cwd} + if self.gitRoot is not None: + result["gitRoot"] = self.gitRoot + if self.repository is not None: + result["repository"] = self.repository + if self.branch is not None: + result["branch"] = self.branch + return result + + +@dataclass +class SessionListFilter: + """Filter options for listing sessions""" + + cwd: str | None = None # Filter by exact cwd match + gitRoot: str | None = None # Filter by git root + repository: str | None = None # Filter by repository (owner/repo format) + branch: str | None = None # Filter by branch + + def to_dict(self) -> dict: + result: dict = {} + if self.cwd is not None: + result["cwd"] = self.cwd + if self.gitRoot is not None: + result["gitRoot"] = self.gitRoot + if self.repository is not None: + result["repository"] = self.repository + if self.branch is not None: + result["branch"] = self.branch + return result + + @dataclass class SessionMetadata: """Metadata about a session""" @@ -927,6 +982,7 @@ class SessionMetadata: modifiedTime: str # ISO 8601 timestamp when session was last modified isRemote: bool # Whether the session is remote summary: str | None = None # Optional summary of the session + context: SessionContext | None = None # Working directory context @staticmethod def from_dict(obj: Any) -> SessionMetadata: @@ -941,12 +997,15 @@ def from_dict(obj: Any) -> SessionMetadata: f"startTime={startTime}, modifiedTime={modifiedTime}, isRemote={isRemote}" ) summary = obj.get("summary") + context_dict = obj.get("context") + context = SessionContext.from_dict(context_dict) if context_dict else None return SessionMetadata( sessionId=str(sessionId), startTime=str(startTime), modifiedTime=str(modifiedTime), isRemote=bool(isRemote), summary=summary, + context=context, ) def to_dict(self) -> dict: @@ -957,6 +1016,8 @@ def to_dict(self) -> dict: result["isRemote"] = self.isRemote if self.summary is not None: result["summary"] = self.summary + if self.context is not None: + result["context"] = self.context.to_dict() return result diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index f2e545ed..58da274b 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -220,6 +220,13 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): assert isinstance(session_data.modifiedTime, str) assert isinstance(session_data.isRemote, bool) + # Verify context field is present + for session_data in sessions: + assert hasattr(session_data, "context") + if session_data.context is not None: + assert hasattr(session_data.context, "cwd") + assert isinstance(session_data.context.cwd, str) + async def test_should_delete_session(self, ctx: E2ETestContext): import asyncio diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index d1725f03..1262e9d6 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^0.0.403", + "@github/copilot": "^0.0.409", "@types/node": "^25.2.0", "openai": "^6.17.0", "tsx": "^4.21.0", @@ -461,27 +461,27 @@ } }, "node_modules/@github/copilot": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.403.tgz", - "integrity": "sha512-v5jUdtGJReLmE1rmff/LZf+50nzmYQYAaSRNtVNr9g0j0GkCd/noQExe31i1+PudvWU0ZJjltR0B8pUfDRdA9Q==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.409.tgz", + "integrity": "sha512-rkYWOKjTSuGg99KsgmA0QAP4X2cpJzAYk6lZDlVxKPhuLP03wC5E+jLctrSLjpxhX32p9n13rm1+7Jun80a1hw==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "0.0.403", - "@github/copilot-darwin-x64": "0.0.403", - "@github/copilot-linux-arm64": "0.0.403", - "@github/copilot-linux-x64": "0.0.403", - "@github/copilot-win32-arm64": "0.0.403", - "@github/copilot-win32-x64": "0.0.403" + "@github/copilot-darwin-arm64": "0.0.409", + "@github/copilot-darwin-x64": "0.0.409", + "@github/copilot-linux-arm64": "0.0.409", + "@github/copilot-linux-x64": "0.0.409", + "@github/copilot-win32-arm64": "0.0.409", + "@github/copilot-win32-x64": "0.0.409" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.403.tgz", - "integrity": "sha512-dOw8IleA0d1soHnbr/6wc6vZiYWNTKMgfTe/NET1nCfMzyKDt/0F0I7PT5y+DLujJknTla/ZeEmmBUmliTW4Cg==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.409.tgz", + "integrity": "sha512-yjrrp++UNNvRoWsZ1+UioBqb3DEVxL5M5ePnMO5/Sf1sngxh0y5P9P6ePFZU4PVlM5BgC38DtrcauZaKf/oArQ==", "cpu": [ "arm64" ], @@ -496,9 +496,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.403.tgz", - "integrity": "sha512-aK2jSNWgY8eiZ+TmrvGhssMCPDTKArc0ip6Ul5OaslpytKks8hyXoRbxGD0N9sKioSUSbvKUf+1AqavbDpJO+w==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.409.tgz", + "integrity": "sha512-EhLfY5DGU/BZmwjVcfnwKuJA7BxS9zdNCGeynUq7z/SI93ziastFqOddUX4D+ySz6yMrrXieN8cUKgzAlRCOJg==", "cpu": [ "x64" ], @@ -513,9 +513,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.403.tgz", - "integrity": "sha512-KhoR2iR70O6vCkzf0h8/K+p82qAgOvMTgAPm9bVEHvbdGFR7Py9qL5v03bMbPxsA45oNaZAkzDhfTAqWhIAZsQ==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.409.tgz", + "integrity": "sha512-O7b/9LmBO8ljPqNngonx+v5d3cOs6HKvj2E9f5/Flb9Uw2lut7g6KGerfDYCMZUpvFCMDfbZSBJD3SDuJj1uPg==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.403.tgz", - "integrity": "sha512-eoswUc9vo4TB+/9PgFJLVtzI4dPjkpJXdCsAioVuoqPdNxHxlIHFe9HaVcqMRZxUNY1YHEBZozy+IpUEGjgdfQ==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.409.tgz", + "integrity": "sha512-zSfFqyPxNaBE5/ClrSjsKxhhTpJaVOqSJY0q87iV9fw6xwdzcJ1/FlZGKjE7W8YVb4tdJx+OBMjQCU8WYewF1A==", "cpu": [ "x64" ], @@ -547,9 +547,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.403.tgz", - "integrity": "sha512-djWjzCsp2xPNafMyOZ/ivU328/WvWhdroGie/DugiJBTgQL2SP0quWW1fhTlDwE81a3g9CxfJonaRgOpFTJTcg==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.409.tgz", + "integrity": "sha512-VizZsdK7L3ym/OR4wahiFx+6hFtaOYN9qvsHmNSo8pb65AZ6ORdRnCPE7w9ZejMpdNEa6x6WqHfxDKJlF85zyA==", "cpu": [ "arm64" ], @@ -564,9 +564,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.403.tgz", - "integrity": "sha512-lju8cHy2E6Ux7R7tWyLZeksYC2MVZu9i9ocjiBX/qfG2/pNJs7S5OlkwKJ0BSXSbZEHQYq7iHfEWp201bVfk9A==", + "version": "0.0.409", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.409.tgz", + "integrity": "sha512-c6dP3XRFk550PmH1Vxe7n/bStNSLnVGH5B+ErUKXk/SPqmZ59pyoa7H2USNdoC6Nav5tkwYYR1vwNZRy+iKvrA==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index 7a1a37ad..9c75747b 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -11,7 +11,7 @@ "test": "vitest run" }, "devDependencies": { - "@github/copilot": "^0.0.403", + "@github/copilot": "^0.0.409", "@types/node": "^25.2.0", "openai": "^6.17.0", "tsx": "^4.21.0",