-
Notifications
You must be signed in to change notification settings - Fork 37.3k
feat-internal: Export Chat as Zip with Repository Context #286812
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a new feature for internal users to export chat sessions as zip files, including repository information when available. The export captures the chat conversation data along with contextual information about the Git repository state at the time the chat session was started.
Key changes:
- Adds a new
ChatRepoInfoContributionthat captures repository context (remote URL, commit hash, changed files) when a chat request is first submitted - Implements an "Export Chat as Zip" action that creates a zip file containing chat data and optional repository information
- Extends the chat model interfaces to store and serialize repository data alongside chat sessions
- Adds native service support for creating zip files from in-memory file content
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/chat/common/model/chatModel.ts | Adds IExportableRepoData and IExportableRepoDiff interfaces; extends ChatModel to store and serialize repository data |
| src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts | New contribution that captures git repository information on first chat request |
| src/vs/workbench/contrib/chat/browser/chat.contribution.ts | Registers the ChatRepoInfoContribution |
| src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts | Implements the export chat as zip action with repository data |
| src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts | Registers the export zip action |
| src/vs/platform/native/common/native.ts | Adds createZipFile method to native service interface |
| src/vs/platform/native/electron-main/nativeHostMainService.ts | Implements createZipFile using existing zip utility |
| src/vs/workbench/test/electron-browser/workbenchTestServices.ts | Updates test service mock with createZipFile stub |
| src/vs/workbench/contrib/chat/test/common/model/mockChatModel.ts | Updates MockChatModel with repoData property and setRepoData method |
Comments suppressed due to low confidence (2)
src/vs/workbench/contrib/chat/common/model/chatModel.ts:1339
- The repoData field was added to ISerializableChatData3 without a comment explaining what it is or noting backward compatibility concerns, while the inputState field above had such a comment (which was removed in this change). Consider adding a comment like "/** Repository context captured when the session started (optional, fully backwards compatible) */" to maintain consistency and help future maintainers understand the field's purpose and compatibility guarantees.
repoData?: IExportableRepoData;
src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts:123
- The ChatRepoInfoContribution lacks test coverage. Since this contribution performs important operations like reading git configuration, capturing repository state, and handling various edge cases (missing git history, worktrees, etc.), consider adding tests to verify the repo data capture logic and error handling behavior.
static readonly ID = 'workbench.contrib.chatRepoInfo';
constructor(
@IChatService private readonly chatService: IChatService,
@ISCMService private readonly scmService: ISCMService,
@IFileService private readonly fileService: IFileService,
@ILogService private readonly logService: ILogService,
) {
super();
this._register(this.chatService.onDidSubmitRequest(async ({ chatSessionResource }) => {
const model = this.chatService.getSession(chatSessionResource);
if (!model || model.repoData) {
return;
}
await this.captureAndSetRepoData(model);
}));
}
private async captureAndSetRepoData(model: IChatModel): Promise<void> {
try {
const repoData = await this.captureRepoInfo();
if (repoData) {
model.setRepoData(repoData);
if (!repoData.headCommitHash) {
this.logService.warn('[ChatRepoInfo] Captured repo data without commit hash - git history may not be ready');
}
} else {
this.logService.debug('[ChatRepoInfo] No SCM repository available for chat session');
}
} catch (error) {
this.logService.warn('[ChatRepoInfo] Failed to capture repo info:', error);
}
}
private async captureRepoInfo(): Promise<IExportableRepoData | undefined> {
// Get the first SCM repository
const repositories = [...this.scmService.repositories];
if (repositories.length === 0) {
return undefined;
}
const repository = repositories[0];
const rootUri = repository.provider.rootUri;
if (!rootUri) {
return undefined;
}
let remoteUrl: string | undefined;
try {
const gitConfigUri = rootUri.with({ path: `${rootUri.path}/.git/config` });
const exists = await this.fileService.exists(gitConfigUri);
if (exists) {
const content = await this.fileService.readFile(gitConfigUri);
const remotes = getRemotes(content.value.toString());
remoteUrl = remotes[0];
}
} catch (error) {
this.logService.warn('[ChatRepoInfo] Failed to read git remote URL:', error);
}
let branchName: string | undefined;
let headCommitHash: string | undefined;
const historyProvider = repository.provider.historyProvider?.get();
if (historyProvider) {
const historyItemRef = historyProvider.historyItemRef.get();
branchName = historyItemRef?.name;
headCommitHash = historyItemRef?.revision;
}
let repoType: 'github' | 'ado' = 'github';
if (remoteUrl?.includes('dev.azure.com') || remoteUrl?.includes('visualstudio.com')) {
repoType = 'ado';
}
const diffs: IExportableRepoDiff[] = [];
let changedFileCount = 0;
for (const group of repository.provider.groups) {
for (const resource of group.resources) {
changedFileCount++;
diffs.push({
uri: resource.sourceUri.toString(),
originalUri: resource.multiDiffEditorOriginalUri?.toString() ?? resource.sourceUri.toString(),
renameUri: undefined,
status: group.label || group.id,
diff: undefined
});
}
}
return {
remoteUrl,
repoType,
branchName,
headCommitHash,
changedFileCount,
diffs
};
}
}
src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts
Outdated
Show resolved
Hide resolved
src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts
Outdated
Show resolved
Hide resolved
src/vs/workbench/contrib/chat/test/common/model/mockChatModel.ts
Outdated
Show resolved
Hide resolved
b651569 to
4f6916f
Compare
📬 CODENOTIFYThe following users are being notified based on files changed in this PR: @bpaseroMatched files:
|
| } | ||
|
|
||
| try { | ||
| await nativeHostService.createZipFile(result.fsPath, files); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's pass around URIs, not string paths
| if (model.repoData) { | ||
| return; | ||
| } | ||
| await this.captureAndSetRepoData(model); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like this always captures the repo data for every chat model, every user? But this seems like it coudl be really expensive? Should it only happen when the user requests it?
| try { | ||
| const repoData = await captureRepoInfo(this.scmService, this.fileService); | ||
| if (repoData) { | ||
| model.setRepoData(repoData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we do this multiple times per ChatModel, and just keep replacing the previous repo data? I don't understand that part.
|
|
||
| // Clean up when the model is disposed | ||
| disposables.add(model.onDidDispose(() => { | ||
| disposables.dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You dispose disposables but it's still in _pendingSessions so it would leak the memory
| this._onDidDisposeSession.fire({ sessionResource: [model.sessionResource], reason: 'cleared' }); | ||
| })); | ||
| this._register(this._sessionModels.onDidCreateModel(model => { | ||
| this._onDidCreateModel.fire(model); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can expose this._sessionModels.onDidCreateModel rather than create a new event emitter. eg onDidCreateModel = this._sessionModels.onDidCreateModel
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it has to be returned from a getter
bpasero
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a minor comment, no objections having a zip method in native host service, thats easier than having it anywhere else.
| //#region Zip | ||
|
|
||
| async createZipFile(windowId: number | undefined, zipPath: string, files: { path: string; contents: string }[]): Promise<void> { | ||
| const { zip } = await import('../../../base/node/zip.js'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to lazy import this.
Adds "Export Chat as Zip" action to package chat sessions with repository context for benchmark creation.
What it does
chat.json(session data) andrepos/*.json(repo state per workspace folder)git applyWorkspace Scenarios
This PR implements the synced scenario. Other scenarios require git bundle support (future work).
Sample repo.json
{ "workspaceType": "remote-git", "syncStatus": "synced", "remoteUrl": "https://github.com/example-org/my-project.git", "remoteVendor": "github", "remoteTrackingBranch": "origin/feature/chat-export", "remoteBaseBranch": "origin/main", "remoteHeadCommit": "abc123def456", "localBranch": "feature/chat-export", "localHeadCommit": "abc123def456", "diffs": [ { "relativePath": "src/utils/helper.ts", "changeType": "modified", "status": "M", "unifiedDiff": "--- a/src/utils/helper.ts\n+++ b/src/utils/helper.ts\n@@ -10,6 +10,7 @@\n function helper() {\n+ console.log('debug');\n return true;\n }" } ] }