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
18 changes: 8 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1652,15 +1652,6 @@
]
}
]
},
{
"id": "github.copilot.chatReplay",
"name": "chatReplay",
"fullName": "Chat Replay",
"when": "debugType == 'vscode-chat-replay'",
"locations": [
"panel"
]
}
],
"languageModelChatProviders": [
Expand Down Expand Up @@ -4056,6 +4047,13 @@
"description": "Complete a security review of the pending changes on the current branch"
}
]
},
{
"id": "chat-replay",
"type": "chat-replay",
"name": "replay",
"displayName": "Chat Replay",
"description": "Replay chat sessions from JSON files"
}
],
"debuggers": [
Expand Down Expand Up @@ -4267,4 +4265,4 @@
"string_decoder": "npm:[email protected]",
"node-gyp": "npm:[email protected]"
}
}
}
8 changes: 0 additions & 8 deletions src/extension/conversation/vscode-node/chatParticipants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ class ChatAgents implements IDisposable {
this._disposables.add(this.registerVSCodeAgent());
this._disposables.add(this.registerTerminalAgent());
this._disposables.add(this.registerTerminalPanelAgent());
this._disposables.add(this.registerReplayAgent());
}

private createAgent(name: string, defaultIntentIdOrGetter: IntentOrGetter, options?: { id?: string }): vscode.ChatParticipant {
Expand Down Expand Up @@ -250,13 +249,6 @@ Learn more about [GitHub Copilot](https://docs.github.com/copilot/using-github-c
return defaultAgent;
}

private registerReplayAgent(): IDisposable {
const defaultAgent = this.createAgent('chatReplay', Intent.ChatReplay);
defaultAgent.iconPath = new vscode.ThemeIcon('copilot');

return defaultAgent;
}

private getChatParticipantHandler(id: string, name: string, defaultIntentIdOrGetter: IntentOrGetter, onRequestPaused: Event<vscode.ChatParticipantPauseStateEvent>): vscode.ChatExtendedRequestHandler {
return async (request, context, stream, token): Promise<vscode.ChatResult> => {

Expand Down
4 changes: 1 addition & 3 deletions src/extension/intents/node/allIntents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { SyncDescriptor } from '../../../util/vs/platform/instantiation/common/d
import { IntentRegistry } from '../../prompt/node/intentRegistry';
import { AgentIntent } from './agentIntent';
import { AskAgentIntent } from './askAgentIntent';
import { ChatReplayIntent } from './chatReplayIntent';
import { InlineDocIntent } from './docIntent';
import { EditCodeIntent } from './editCodeIntent';
import { EditCode2Intent } from './editCodeIntent2';
Expand Down Expand Up @@ -54,6 +53,5 @@ IntentRegistry.setIntents([
new SyncDescriptor(SearchPanelIntent),
new SyncDescriptor(SearchKeywordsIntent),
new SyncDescriptor(AskAgentIntent),
new SyncDescriptor(NotebookEditorIntent),
new SyncDescriptor(ChatReplayIntent)
new SyncDescriptor(NotebookEditorIntent)
]);
2 changes: 1 addition & 1 deletion src/extension/intents/node/chatReplayIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class ChatReplayIntent implements IIntent {
break;
case 'toolCall':
{
replay.setToolResult(step.id, step.results);

const result = await this.toolsService.invokeTool(ToolName.ToolReplay,
{
toolInvocationToken: toolToken,
Expand Down
11 changes: 1 addition & 10 deletions src/extension/replay/common/chatReplayResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,14 @@ type Request = {
id: string;
line: number;
prompt: string;
result: string;
result: string | string[];
}

export type ChatStep = UserQuery | Request | ToolStep;

export class ChatReplayResponses {
private pendingRequests: DeferredPromise<ChatStep | 'finished'>[] = [];
private responses: (ChatStep | 'finished')[] = [];
private toolResults: Map<string, string[]> = new Map();

public static instance: ChatReplayResponses;

Expand Down Expand Up @@ -88,14 +87,6 @@ export class ChatReplayResponses {
return deferred.p;
}

public setToolResult(id: string, result: string[]): void {
this.toolResults.set(id, result);
}

public getToolResult(id: string): string[] | undefined {
return this.toolResults.get(id);
}

public markDone(): void {
while (this.pendingRequests.length > 0) {
const waiter = this.pendingRequests.shift();
Expand Down
159 changes: 159 additions & 0 deletions src/extension/replay/common/replayParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fs from 'node:fs';
import { ChatStep } from './chatReplayResponses';

export interface ReplayData {
chatSteps: ChatStep[];
filePath: string;
}

/**
* Parses a replay file and returns the chat steps with line numbers
* @param filePath The absolute path to the replay file
* @returns The parsed replay data containing chat steps and file path
*/
export function parseReplayFromFile(filePath: string): ReplayData {
if (!fs.existsSync(filePath)) {
throw new Error(`Replay file not found: ${filePath}`);
}

try {
const content = fs.readFileSync(filePath, 'utf8');
const chatSteps = parseReplayContent(content);
return {
chatSteps,
filePath
};
} catch (error) {
throw new Error(`Failed to parse replay file ${filePath}: ${error}`);
}
}

/**
* Parses a replay file from a session ID (base64 encoded file path)
* @param sessionId The session ID (base64 encoded file path, optionally prefixed with 'debug:')
* @returns The parsed replay data containing chat steps and file path
*/
export function parseReplayFromSessionId(sessionId: string): ReplayData {
const filePath = getFilePathFromSessionId(sessionId);
if (!filePath) {
throw new Error(`Invalid session ID: ${sessionId}`);
}
return parseReplayFromFile(filePath);
}

/**
* Converts a session ID to a file path
* @param sessionId The session ID (base64 encoded file path, optionally prefixed with 'debug:')
* @returns The decoded file path, or undefined if the session ID is invalid
*/
export function getFilePathFromSessionId(sessionId: string): string | undefined {
try {
// Handle debug session IDs by removing the debug prefix
const actualSessionId = sessionId.startsWith('debug:') ? sessionId.substring(6) : sessionId;
return Buffer.from(actualSessionId, 'base64').toString('utf8');
} catch {
return undefined;
}
}

/**
* Creates a session ID from a file path
* @param filePath The absolute path to the replay file
* @param isDebugSession Whether this is a debug session (adds 'debug:' prefix)
* @returns The base64 encoded session ID
*/
export function createSessionIdFromFilePath(filePath: string): string {
return Buffer.from(filePath).toString('base64');
}

/**
* Parses the replay content and assigns line numbers to each step
* @param content The raw replay file content
* @returns Array of chat steps with line numbers
*/
function parseReplayContent(content: string): ChatStep[] {
const parsed = JSON.parse(content);
const prompts = (parsed.prompts && Array.isArray(parsed.prompts) ? parsed.prompts : [parsed]) as { [key: string]: any }[];

if (prompts.filter(p => !p.prompt).length) {
throw new Error('Invalid replay content: expected a prompt object or an array of prompts in the base JSON structure.');
}

const steps: ChatStep[] = [];
for (const prompt of prompts) {
steps.push(...parsePrompt(prompt));
}

// Assign line numbers based on content
assignLineNumbers(steps, content);

return steps;
}

/**
* Parses a single prompt object into chat steps
*/
function parsePrompt(prompt: { [key: string]: any }): ChatStep[] {
const steps: ChatStep[] = [];
steps.push({
kind: 'userQuery',
query: prompt.prompt,
line: 0,
});

for (const log of prompt.logs) {
if (log.kind === 'toolCall') {
steps.push({
kind: 'toolCall',
id: log.id,
line: 0,
toolName: log.tool,
args: JSON.parse(log.args),
edits: log.edits,
results: log.response
});
} else if (log.kind === 'request') {
steps.push({
kind: 'request',
id: log.id,
line: 0,
prompt: log.messages,
result: log.response.message
});
}
}

return steps;
}

/**
* Assigns line numbers to steps based on their location in the content
*/
function assignLineNumbers(steps: ChatStep[], content: string): void {
let stepIx = 0;
const lines = content.split('\n');

lines.forEach((line, index) => {
if (stepIx < steps.length) {
const step = steps[stepIx];
if (step.kind === 'userQuery') {
const match = line.match(`"prompt": "${step.query.trim()}`);
if (match) {
step.line = index + 1;
stepIx++;
}
} else {
const match = line.match(`"id": "${step.id}"`);
if (match) {
step.line = index + 1;
stepIx++;
}
}
}
});
}
Loading