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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions src/extension/agents/copilotcli/node/copilotcliSessionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<ICopilotCLISessionItem | undefined> => {
if (!this._sessionTracker.shouldShowSession(metadata.sessionId)) {
return;
}
Expand All @@ -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
Expand All @@ -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}`);
}
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
142 changes: 128 additions & 14 deletions src/extension/vscode.proposed.chatSessionsProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>): ChatSessionItemController;
}

/**
* Provides a list of information about chat sessions.
*/
Expand All @@ -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<void>;

/**
* Fired when an item is archived by the editor
*
* TODO: expose archive state on the item too?
*/
readonly onDidArchiveChatSessionItem: Event<ChatSessionItem>;
}

/**
* A collection of chat session items. It provides operations for managing and iterating over the items.
*/
export interface ChatSessionItemCollection extends Iterable<readonly [id: Uri, chatSessionItem: ChatSessionItem]> {
/**
* 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.
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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}.
*
Expand Down
Loading