diff --git a/src/extension/agents/copilotcli/node/copilotcliSessionService.ts b/src/extension/agents/copilotcli/node/copilotcliSessionService.ts index a2564a5986..e3ec77f7a8 100644 --- a/src/extension/agents/copilotcli/node/copilotcliSessionService.ts +++ b/src/extension/agents/copilotcli/node/copilotcliSessionService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { internal, Session, SessionEvent, SessionOptions, SweCustomAgent } from '@github/copilot/sdk'; -import type { CancellationToken, ChatRequest, Uri } from 'vscode'; +import type { CancellationToken, ChatRequest, ChatSessionItem, Uri } from 'vscode'; import { INativeEnvService } from '../../../../platform/env/common/envService'; import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext'; import { createDirectoryIfNotExists, IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService'; @@ -32,7 +32,7 @@ const COPILOT_CLI_WORKSPACE_JSON_FILE_KEY = 'github.copilot.cli.workspaceSession export interface ICopilotCLISessionItem { readonly id: string; readonly label: string; - readonly timing: { startTime: number; endTime?: number }; + readonly timing: ChatSessionItem['timing']; readonly status?: ChatSessionStatus; } @@ -124,7 +124,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS await this._sessionTracker.initialize(sessionMetadataList.map(s => s.sessionId)); // Convert SessionMetadata to ICopilotCLISession const diskSessions: ICopilotCLISessionItem[] = coalesce(await Promise.all( - sessionMetadataList.map(async (metadata) => { + sessionMetadataList.map(async (metadata): Promise => { if (!this._sessionTracker.shouldShowSession(metadata.sessionId)) { return; } @@ -139,8 +139,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS return { id, label, - timing: { startTime, endTime }, - } satisfies ICopilotCLISessionItem; + timing: { created: startTime, startTime, endTime }, + }; } try { // Get the full session to access chat messages @@ -156,8 +156,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS return { id, label, - timing: { startTime, endTime }, - } satisfies ICopilotCLISessionItem; + timing: { created: startTime, startTime, endTime }, + }; } catch (error) { this.logService.warn(`Failed to load session ${metadata.sessionId}: ${error}`); } @@ -170,27 +170,28 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS const newSessions = coalesce(Array.from(this._sessionWrappers.values()) .filter(session => !diskSessionIds.has(session.object.sessionId)) .filter(session => session.object.status === ChatSessionStatus.InProgress) - .map(session => { + .map((session): ICopilotCLISessionItem | undefined => { const label = labelFromPrompt(session.object.pendingPrompt ?? ''); if (!label) { return; } + const createTime = Date.now(); return { id: session.object.sessionId, label, status: session.object.status, - timing: { startTime: Date.now() }, - } satisfies ICopilotCLISessionItem; + timing: { created: createTime, startTime: createTime }, + }; })); // Merge with cached sessions (new sessions not yet persisted by SDK) const allSessions = diskSessions - .map(session => { + .map((session): ICopilotCLISessionItem => { return { ...session, status: this._sessionWrappers.get(session.id)?.object?.status - } satisfies ICopilotCLISessionItem; + }; }).concat(newSessions); return allSessions; diff --git a/src/extension/chatSessions/vscode-node/claudeChatSessionItemProvider.ts b/src/extension/chatSessions/vscode-node/claudeChatSessionItemProvider.ts index d11e6c5307..5f3c1cf7ee 100644 --- a/src/extension/chatSessions/vscode-node/claudeChatSessionItemProvider.ts +++ b/src/extension/chatSessions/vscode-node/claudeChatSessionItemProvider.ts @@ -43,7 +43,8 @@ export class ClaudeChatSessionItemProvider extends Disposable implements vscode. label: session.label, tooltip: `Claude Code session: ${session.label}`, timing: { - startTime: session.timestamp.getTime() + created: session.timestamp.getTime(), + startTime: session.timestamp.getTime(), }, iconPath: new vscode.ThemeIcon('star-add') } satisfies vscode.ChatSessionItem)); diff --git a/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts b/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts index ffe04db06e..e276e2bb9b 100644 --- a/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts +++ b/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts @@ -670,6 +670,7 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C tooltip: this.createPullRequestTooltip(pr), ...(createdAt ? { timing: { + created: createdAt, startTime: createdAt, endTime: validateISOTimestamp(sessionItem.completed_at), } diff --git a/src/extension/vscode.proposed.chatSessionsProvider.d.ts b/src/extension/vscode.proposed.chatSessionsProvider.d.ts index ac6ade0f41..016b45c291 100644 --- a/src/extension/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/extension/vscode.proposed.chatSessionsProvider.d.ts @@ -26,6 +26,25 @@ declare module 'vscode' { InProgress = 2 } + export namespace chat { + /** + * Registers a new {@link ChatSessionItemProvider chat session item provider}. + * + * To use this, also make sure to also add `chatSessions` contribution in the `package.json`. + * + * @param chatSessionType The type of chat session the provider is for. + * @param provider The provider to register. + * + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerChatSessionItemProvider(chatSessionType: string, provider: ChatSessionItemProvider): Disposable; + + /** + * Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier. + */ + export function createChatSessionItemController(id: string, refreshHandler: () => Thenable): ChatSessionItemController; + } + /** * Provides a list of information about chat sessions. */ @@ -52,6 +71,86 @@ declare module 'vscode' { // #endregion } + /** + * Provides a list of information about chat sessions. + */ + export interface ChatSessionItemController { + readonly id: string; + + /** + * Unregisters the controller, disposing of its associated chat session items. + */ + dispose(): void; + + /** + * Managed collection of chat session items + */ + readonly items: ChatSessionItemCollection; + + /** + * Creates a new managed chat session item that be added to the collection. + */ + createChatSessionItem(resource: Uri, label: string): ChatSessionItem; + + /** + * Handler called to refresh the collection of chat session items. + * + * This is also called on first load to get the initial set of items. + */ + refreshHandler: () => Thenable; + + /** + * Fired when an item is archived by the editor + * + * TODO: expose archive state on the item too? + */ + readonly onDidArchiveChatSessionItem: Event; + } + + /** + * A collection of chat session items. It provides operations for managing and iterating over the items. + */ + export interface ChatSessionItemCollection extends Iterable { + /** + * Gets the number of items in the collection. + */ + readonly size: number; + + /** + * Replaces the items stored by the collection. + * @param items Items to store. + */ + replace(items: readonly ChatSessionItem[]): void; + + /** + * Iterate over each entry in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callback: (item: ChatSessionItem, collection: ChatSessionItemCollection) => unknown, thisArg?: any): void; + + /** + * Adds the chat session item to the collection. If an item with the same resource URI already + * exists, it'll be replaced. + * @param item Item to add. + */ + add(item: ChatSessionItem): void; + + /** + * Removes a single chat session item from the collection. + * @param resource Item resource to delete. + */ + delete(resource: Uri): void; + + /** + * Efficiently gets a chat session item by resource, if it exists, in the collection. + * @param resource Item resource to get. + * @returns The found item or undefined if it does not exist. + */ + get(resource: Uri): ChatSessionItem | undefined; + } + export interface ChatSessionItem { /** * The resource associated with the chat session. @@ -91,15 +190,42 @@ declare module 'vscode' { tooltip?: string | MarkdownString; /** - * The times at which session started and ended + * Whether the chat session has been archived. + */ + archived?: boolean; + + /** + * Timing information for the chat session */ timing?: { + /** + * Timestamp when the session was created in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + created: number; + + /** + * Timestamp when the most recent request started in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * Should be undefined if no requests have been made yet. + */ + lastRequestStarted?: number; + + /** + * Timestamp when the most recent request completed in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * Should be undefined if the most recent request is still in progress or if no requests have been made yet. + */ + lastRequestEnded?: number; + /** * Session start timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * @deprecated Use `created` and `lastRequestStarted` instead. */ - startTime: number; + startTime?: number; + /** * Session end timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * @deprecated Use `lastRequestEnded` instead. */ endTime?: number; }; @@ -268,18 +394,6 @@ declare module 'vscode' { } export namespace chat { - /** - * Registers a new {@link ChatSessionItemProvider chat session item provider}. - * - * To use this, also make sure to also add `chatSessions` contribution in the `package.json`. - * - * @param chatSessionType The type of chat session the provider is for. - * @param provider The provider to register. - * - * @returns A disposable that unregisters the provider when disposed. - */ - export function registerChatSessionItemProvider(chatSessionType: string, provider: ChatSessionItemProvider): Disposable; - /** * Registers a new {@link ChatSessionContentProvider chat session content provider}. *