From 34afec19ddc0564ab93d22ecdc05b43ff685bc35 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Tue, 26 Aug 2025 08:11:04 -0700 Subject: [PATCH 1/4] new chat session flow prototype --- src/github/copilotApi.ts | 5 ++ src/github/copilotRemoteAgent.ts | 115 +++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/github/copilotApi.ts b/src/github/copilotApi.ts index eee1eff4f5..87dbd3030d 100644 --- a/src/github/copilotApi.ts +++ b/src/github/copilotApi.ts @@ -44,6 +44,11 @@ export interface ChatSessionWithPR extends vscode.ChatSessionItem { pullRequest: PullRequestModel; } +export interface ChatSessionFromSummarizedChat extends vscode.ChatSessionItem { + prompt: string; + summary?: string; +} + export class CopilotApi { protected static readonly ID = 'copilotApi'; diff --git a/src/github/copilotRemoteAgent.ts b/src/github/copilotRemoteAgent.ts index 3d1c521618..a51afbe696 100644 --- a/src/github/copilotRemoteAgent.ts +++ b/src/github/copilotRemoteAgent.ts @@ -19,7 +19,7 @@ import { ITelemetry } from '../common/telemetry'; import { toOpenPullRequestWebviewUri } from '../common/uri'; import { dateFromNow } from '../common/utils'; import { copilotEventToSessionStatus, copilotPRStatusToSessionStatus, IAPISessionLogs, ICopilotRemoteAgentCommandArgs, ICopilotRemoteAgentCommandResponse, OctokitCommon, RemoteAgentResult, RepoInfo } from './common'; -import { ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi'; +import { ChatSessionFromSummarizedChat, ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi'; import { CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher'; import { ChatSessionContentBuilder } from './copilotRemoteAgent/chatSessionContentBuilder'; import { GitOperationsManager } from './copilotRemoteAgent/gitOperationsManager'; @@ -60,6 +60,7 @@ export class CopilotRemoteAgentManager extends Disposable { readonly onDidChangeChatSessions = this._onDidChangeChatSessions.event; private readonly gitOperationsManager: GitOperationsManager; + private ephemeralChatSessions: Map = new Map(); // TODO: Clean these up constructor(private credentialStore: CredentialStore, public repositoriesManager: RepositoriesManager, private telemetry: ITelemetry, private context: vscode.ExtensionContext) { super(); @@ -728,11 +729,28 @@ export class CopilotRemoteAgentManager extends Disposable { return this._stateModel.getCounts(); } - public async provideNewChatSessionItem(options: { prompt?: string; history: ReadonlyArray; metadata?: any; }, _token: vscode.CancellationToken): Promise { + public async provideNewChatSessionItem(options: { prompt?: string; history: ReadonlyArray; metadata?: any; }, _token: vscode.CancellationToken): Promise { const { prompt } = options; if (!prompt) { throw new Error(`Prompt is expected to provide a new chat session item`); } + + const { source, summary } = options.metadata || {}; + + // Ephemeral session for new session creation flow + if (source === 'chatExecuteActions') { + const id = `new-${Date.now()}`; + const val = { + id, + label: vscode.l10n.t('New coding agent session'), + iconPath: new vscode.ThemeIcon('plus'), + prompt, + summary, + }; + this.ephemeralChatSessions.set(id, val); + return val; + } + const result = await this.invokeRemoteAgent( prompt, prompt, @@ -798,6 +816,91 @@ export class CopilotRemoteAgentManager extends Disposable { return []; } + + private async newSessionFlowFromPrompt(id: string): Promise { + const session = this.ephemeralChatSessions.get(id); + if (!session) { + return this.createEmptySession(); + } + + const repoInfo = await this.repoInfo(); + if (!repoInfo) { + return this.createEmptySession(); // TODO: + } + const { repo, owner } = repoInfo; + + // Remove from ephemeral sessions + this.ephemeralChatSessions.delete(id); + + // Create a placeholder session that will invoke the remote agent when confirmed + + const { prompt } = session; + const sessionRequest = new vscode.ChatRequestTurn2( + prompt, + undefined, + [], + COPILOT_SWE_AGENT, + [], + [] + ); + + const placeholderParts = [ + new vscode.ChatResponseProgressPart(vscode.l10n.t('Starting coding agent session...')), + new vscode.ChatResponseConfirmationPart( + vscode.l10n.t('Copilot coding agent will continue your work in \'{0}\'.', `${owner}/${repo}`), + vscode.l10n.t('Your chat context will be used to continue work in a new pull request.'), + 'begin', + ['Continue', 'Cancel'] + ) + ]; + const placeholderTurn = new vscode.ChatResponseTurn2(placeholderParts, {}, COPILOT_SWE_AGENT); + + const parseConfirmationData = (data: any[] | undefined): string[] => { + if (!Array.isArray(data)) { + return []; + } + return data + .map(item => { + const state = item && typeof item.state === 'string' ? item.state : undefined; + return state; + }) + .filter((s): s is string => typeof s === 'string'); + }; + + return { + history: [sessionRequest, placeholderTurn], + requestHandler: async (request: vscode.ChatRequest, _context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise => { + if (token.isCancellationRequested) { + return {}; + } + if (request.acceptedConfirmationData) { + const states = parseConfirmationData(request.acceptedConfirmationData); + for (const state of states) { + switch (state) { + case 'begin': + await this.invokeRemoteAgent( + prompt, + prompt, + false, + ); + return {}; + default: + Logger.error(`Unknown confirmation state: ${state}`, CopilotRemoteAgentManager.ID); + return {}; + } + } + } + if (request.rejectedConfirmationData) { + stream.push(new vscode.ChatResponseProgressPart(vscode.l10n.t('Cancelled starting coding agent session.'))); + return {}; + } + stream.markdown('Pong!'); + return {}; + }, + activeResponseCallback: undefined, + }; + } + public async provideChatSessionContent(id: string, token: vscode.CancellationToken): Promise { try { const capi = await this.copilotApi; @@ -805,14 +908,18 @@ export class CopilotRemoteAgentManager extends Disposable { return this.createEmptySession(); } + await this.waitRepoManagerInitialization(); + + if (id.startsWith('new')) { + return await this.newSessionFlowFromPrompt(id); + } + const pullRequestNumber = parseInt(id); if (isNaN(pullRequestNumber)) { Logger.error(`Invalid pull request number: ${id}`, CopilotRemoteAgentManager.ID); return this.createEmptySession(); } - await this.waitRepoManagerInitialization(); - const pullRequest = await this.findPullRequestById(pullRequestNumber, true); if (!pullRequest) { Logger.error(`Pull request not found: ${pullRequestNumber}`, CopilotRemoteAgentManager.ID); From 34f8716920cab2d098b9babe2ba6b826ed5b684b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:38:16 +0000 Subject: [PATCH 2/4] Initial plan From 1f9d5b5be71de7ab67766d70c0db78f5bdf91042 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:46:05 +0000 Subject: [PATCH 3/4] Initial exploration and plan for test implementation Co-authored-by: joshspicer <23246594+joshspicer@users.noreply.github.com> --- ...ode.proposed.chatParticipantAdditions.d.ts | 20 +++++++++---------- ...scode.proposed.chatParticipantPrivate.d.ts | 2 ++ .../vscode.proposed.chatSessionsProvider.d.ts | 14 +++++++++++++ ...vscode.proposed.languageModelDataPart.d.ts | 4 ++-- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index 772d2fdf90..a07171f511 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -164,10 +164,10 @@ declare module 'vscode' { /** * A specialized progress part for displaying thinking/reasoning steps. */ - export class ChatResponseThinkingProgressPart extends ChatResponseProgressPart { - value: string; + export class ChatResponseThinkingProgressPart { + value: string | string[]; id?: string; - metadata?: string; + metadata?: { readonly [key: string]: any }; task?: (progress: Progress) => Thenable; /** @@ -175,7 +175,7 @@ declare module 'vscode' { * @param value An initial progress message * @param task A task that will emit thinking parts during its execution */ - constructor(value: string, id?: string, metadata?: string, task?: (progress: Progress) => Thenable); + constructor(value: string | string[], id?: string, metadata?: { readonly [key: string]: any }, task?: (progress: Progress) => Thenable); } export class ChatResponseReferencePart2 { @@ -328,18 +328,18 @@ declare module 'vscode' { } export type ThinkingDelta = { - text?: string; + text?: string | string[]; id: string; - metadata?: string; + metadata?: { readonly [key: string]: any }; } | { - text?: string; + text?: string | string[]; id?: string; - metadata: string; + metadata: { readonly [key: string]: any }; } | { - text: string; + text: string | string[]; id?: string; - metadata?: string; + metadata?: { readonly [key: string]: any }; }; export enum ChatResponseClearToPreviousToolInvocationReason { diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts index 5e8b337772..66ae4a6310 100644 --- a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -183,6 +183,8 @@ declare module 'vscode' { isQuotaExceeded?: boolean; level?: ChatErrorLevel; + + code?: string; } export namespace chat { diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index 592fe2ed63..92ba38d36e 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -94,6 +94,20 @@ declare module 'vscode' { * The tooltip text when you hover over this item. */ tooltip?: string | MarkdownString; + + /** + * The times at which session started and ended + */ + timing?: { + /** + * Session start timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + startTime: number; + /** + * Session end timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + endTime?: number; + }; } export interface ChatSession { diff --git a/src/@types/vscode.proposed.languageModelDataPart.d.ts b/src/@types/vscode.proposed.languageModelDataPart.d.ts index d2cd676400..4d491a66ca 100644 --- a/src/@types/vscode.proposed.languageModelDataPart.d.ts +++ b/src/@types/vscode.proposed.languageModelDataPart.d.ts @@ -42,7 +42,7 @@ declare module 'vscode' { * A string or heterogeneous array of things that a message can contain as content. Some parts may be message-type * specific for some models. */ - content: Array; + content: Array; /** * The optional name of a user for this message. @@ -56,7 +56,7 @@ declare module 'vscode' { * @param content The content of the message. * @param name The optional name of a user for the message. */ - constructor(role: LanguageModelChatMessageRole, content: string | Array, name?: string); + constructor(role: LanguageModelChatMessageRole, content: string | Array, name?: string); } /** From 4ffcca022c53e8b8c140193ca12888a926310d3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:49:00 +0000 Subject: [PATCH 4/4] Add test for threadRange utility function Co-authored-by: joshspicer <23246594+joshspicer@users.noreply.github.com> --- src/test/issues/issuesUtils.test.ts | 33 ++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/test/issues/issuesUtils.test.ts b/src/test/issues/issuesUtils.test.ts index 6b48644d14..3841327360 100644 --- a/src/test/issues/issuesUtils.test.ts +++ b/src/test/issues/issuesUtils.test.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { default as assert } from 'assert'; -import { parseIssueExpressionOutput, ISSUE_OR_URL_EXPRESSION } from '../../github/utils'; +import * as vscode from 'vscode'; +import { parseIssueExpressionOutput, ISSUE_OR_URL_EXPRESSION, threadRange } from '../../github/utils'; describe('Issues utilities', function () { it('regular expressions', async function () { @@ -65,4 +66,34 @@ describe('Issues utilities', function () { assert.strictEqual(prUrlHttpParsed?.name, 'repo'); assert.strictEqual(prUrlHttpParsed?.owner, 'owner'); }); + + it('threadRange utility', function () { + // Test same line with default end character + const singleLineRange = threadRange(5, 5); + assert.strictEqual(singleLineRange.start.line, 5); + assert.strictEqual(singleLineRange.start.character, 0); + assert.strictEqual(singleLineRange.end.line, 5); + assert.strictEqual(singleLineRange.end.character, 0); + + // Test different lines without end character (should default to 300) + const multiLineRange = threadRange(5, 10); + assert.strictEqual(multiLineRange.start.line, 5); + assert.strictEqual(multiLineRange.start.character, 0); + assert.strictEqual(multiLineRange.end.line, 10); + assert.strictEqual(multiLineRange.end.character, 300); + + // Test different lines with specific end character + const multiLineRangeWithChar = threadRange(5, 10, 25); + assert.strictEqual(multiLineRangeWithChar.start.line, 5); + assert.strictEqual(multiLineRangeWithChar.start.character, 0); + assert.strictEqual(multiLineRangeWithChar.end.line, 10); + assert.strictEqual(multiLineRangeWithChar.end.character, 25); + + // Test same line with specific end character + const singleLineRangeWithChar = threadRange(3, 3, 15); + assert.strictEqual(singleLineRangeWithChar.start.line, 3); + assert.strictEqual(singleLineRangeWithChar.start.character, 0); + assert.strictEqual(singleLineRangeWithChar.end.line, 3); + assert.strictEqual(singleLineRangeWithChar.end.character, 15); + }); });