Skip to content
Draft
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
20 changes: 10 additions & 10 deletions src/@types/vscode.proposed.chatParticipantAdditions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,18 +164,18 @@ 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<LanguageModelThinkingPart>) => Thenable<string | void>;

/**
* Creates a new thinking progress part.
* @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<LanguageModelThinkingPart>) => Thenable<string | void>);
constructor(value: string | string[], id?: string, metadata?: { readonly [key: string]: any }, task?: (progress: Progress<LanguageModelThinkingPart>) => Thenable<string | void>);
}

export class ChatResponseReferencePart2 {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/@types/vscode.proposed.chatParticipantPrivate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ declare module 'vscode' {
isQuotaExceeded?: boolean;

level?: ChatErrorLevel;

code?: string;
}

export namespace chat {
Expand Down
14 changes: 14 additions & 0 deletions src/@types/vscode.proposed.chatSessionsProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/@types/vscode.proposed.languageModelDataPart.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart>;
content: Array<LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelThinkingPart>;

/**
* The optional name of a user for this message.
Expand All @@ -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<LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart>, name?: string);
constructor(role: LanguageModelChatMessageRole, content: string | Array<LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelThinkingPart>, name?: string);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/github/copilotApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
115 changes: 111 additions & 4 deletions src/github/copilotRemoteAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -60,6 +60,7 @@ export class CopilotRemoteAgentManager extends Disposable {
readonly onDidChangeChatSessions = this._onDidChangeChatSessions.event;

private readonly gitOperationsManager: GitOperationsManager;
private ephemeralChatSessions: Map<string, ChatSessionFromSummarizedChat> = new Map(); // TODO: Clean these up

constructor(private credentialStore: CredentialStore, public repositoriesManager: RepositoriesManager, private telemetry: ITelemetry, private context: vscode.ExtensionContext) {
super();
Expand Down Expand Up @@ -728,11 +729,28 @@ export class CopilotRemoteAgentManager extends Disposable {
return this._stateModel.getCounts();
}

public async provideNewChatSessionItem(options: { prompt?: string; history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>; metadata?: any; }, _token: vscode.CancellationToken): Promise<ChatSessionWithPR> {
public async provideNewChatSessionItem(options: { prompt?: string; history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>; metadata?: any; }, _token: vscode.CancellationToken): Promise<ChatSessionWithPR | ChatSessionFromSummarizedChat> {
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,
Expand Down Expand Up @@ -798,21 +816,110 @@ export class CopilotRemoteAgentManager extends Disposable {
return [];
}


private async newSessionFlowFromPrompt(id: string): Promise<vscode.ChatSession> {
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<vscode.ChatResult> => {
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<vscode.ChatSession> {
try {
const capi = await this.copilotApi;
if (!capi || token.isCancellationRequested) {
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);
Expand Down
33 changes: 32 additions & 1 deletion src/test/issues/issuesUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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);
});
});