Skip to content
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

Enable chat session export/import #182562

Merged
merged 2 commits into from May 16, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,108 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { VSBuffer } from 'vs/base/common/buffer';
import { joinPath } from 'vs/base/common/resources';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IFileService } from 'vs/platform/files/common/files';
import { INTERACTIVE_SESSION_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
import { isSerializableSessionData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';

const defaultFileName = 'chat.json';
const filters = [{ name: localize('interactiveSession.file.label', "Chat Session"), extensions: ['json'] }];

export function registerInteractiveSessionExportActions() {
registerAction2(class ExportInteractiveSessionAction extends Action2 {
constructor() {
super({
id: 'workbench.action.interactiveSession.export',
category: INTERACTIVE_SESSION_CATEGORY,
title: {
value: localize('interactiveSession.export.label', "Export Session") + '...',
original: 'Export Session...'
},
f1: true,
});
}
async run(accessor: ServicesAccessor, ...args: any[]) {
const widgetService = accessor.get(IChatWidgetService);
const fileDialogService = accessor.get(IFileDialogService);
const fileService = accessor.get(IFileService);
const interactiveSessionService = accessor.get(IChatService);

const widget = widgetService.lastFocusedWidget;
if (!widget || !widget.viewModel) {
return;
}

const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultFileName);
const result = await fileDialogService.showSaveDialog({
defaultUri,
filters
});
if (!result) {
return;
}

const model = interactiveSessionService.getSession(widget.viewModel.sessionId);
if (!model) {
return;
}

// Using toJSON on the model
const content = VSBuffer.fromString(JSON.stringify(model, undefined, 2));
await fileService.writeFile(result, content);
}
});

registerAction2(class ImportInteractiveSessionAction extends Action2 {
constructor() {
super({
id: 'workbench.action.interactiveSession.import',
title: {
value: localize('interactiveSession.import.label', "Import Session") + '...',
original: 'Export Session...'
},
category: INTERACTIVE_SESSION_CATEGORY,
f1: true,
});
}
async run(accessor: ServicesAccessor, ...args: any[]) {
const fileDialogService = accessor.get(IFileDialogService);
const fileService = accessor.get(IFileService);
const editorService = accessor.get(IEditorService);

const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultFileName);
const result = await fileDialogService.showOpenDialog({
defaultUri,
canSelectFiles: true,
filters
});
if (!result) {
return;
}

const content = await fileService.readFile(result[0]);
try {
const data = JSON.parse(content.value.toString());
if (!isSerializableSessionData(data)) {
throw new Error('Invalid chat session data');
}

await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: <IChatEditorOptions>{ target: { data }, pinned: true } });
} catch (err) {
throw err;
}
}
});
}
12 changes: 7 additions & 5 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { isMacintosh } from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
Expand All @@ -18,17 +19,19 @@ import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/c
import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions';
import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions';
import { registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions';
import { registerChatQuickQuestionActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions';
import { registerInteractiveSessionExportActions } from 'vs/workbench/contrib/chat/browser/actions/interactiveSessionImportExport';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl';
import { IChatEditorOptions, ChatEditor } from 'vs/workbench/contrib/chat/browser/chatEditor';
import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget';
import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
import { IChatWidgetHistoryService, ChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService';
import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService';
import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import '../common/chatColors';
Expand Down Expand Up @@ -124,11 +127,10 @@ registerChatCodeBlockActions();
registerChatTitleActions();
registerChatExecuteActions();
registerChatQuickQuestionActions();
registerInteractiveSessionExportActions();

registerSingleton(IChatService, ChatService, InstantiationType.Delayed);
registerSingleton(IChatContributionService, ChatContributionService, InstantiationType.Delayed);
registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed);
registerSingleton(IChatWidgetHistoryService, ChatWidgetHistoryService, InstantiationType.Delayed);

import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib';
import { Schemas } from 'vs/base/common/network';
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/chat/browser/chatEditor.ts
Expand Up @@ -19,11 +19,11 @@ import { Memento } from 'vs/workbench/common/memento';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
import { IViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';

export interface IChatEditorOptions extends IEditorOptions {
target: { sessionId: string } | { providerId: string };
target: { sessionId: string } | { providerId: string } | { data: ISerializableChatData };
}

export class ChatEditor extends EditorPane {
Expand Down
11 changes: 8 additions & 3 deletions src/vs/workbench/contrib/chat/browser/chatEditorInput.ts
Expand Up @@ -69,9 +69,14 @@ export class ChatEditorInput extends EditorInput {
}

override async resolve(): Promise<ChatEditorModel | null> {
const model = typeof this.sessionId === 'string' ?
this.chatService.getOrRestoreSession(this.sessionId) :
this.chatService.startSession(this.providerId!, CancellationToken.None);
let model: IChatModel | undefined;
if (typeof this.sessionId === 'string') {
model = this.chatService.getOrRestoreSession(this.sessionId);
} else if (typeof this.providerId === 'string') {
model = this.chatService.startSession(this.providerId, CancellationToken.None);
} else if ('data' in this.options.target) {
model = this.chatService.loadSessionFromContent(this.options.target.data);
}

if (!model) {
return null;
Expand Down
11 changes: 10 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatModel.ts
Expand Up @@ -198,17 +198,26 @@ export interface ISerializableChatRequestData {

export interface ISerializableChatData {
sessionId: string;
providerId: string;
creationDate: number;
welcomeMessage: (string | IChatReplyFollowup[])[] | undefined;
requests: ISerializableChatRequestData[];
requesterUsername: string;
responderUsername: string;
requesterAvatarIconUri: UriComponents | undefined;
responderAvatarIconUri: UriComponents | undefined;
providerId: string;
providerState: any;
}

export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData {
const data = obj as ISerializableChatData;
return typeof data === 'object' &&
typeof data.providerId === 'string' &&
typeof data.sessionId === 'string' &&
typeof data.requesterUsername === 'string' &&
typeof data.responderUsername === 'string';
}

export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent;

export interface IChatAddRequestEvent {
Expand Down
4 changes: 3 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatService.ts
Expand Up @@ -9,7 +9,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ProviderResult } from 'vs/editor/common/languages';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatModel, ChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatModel, ChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';

export interface IChat {
id: number; // TODO Maybe remove this and move to a subclass that only the provider knows about
Expand Down Expand Up @@ -177,7 +177,9 @@ export interface IChatService {
registerSlashCommandProvider(provider: ISlashCommandProvider): IDisposable;
getProviderInfos(): IChatProviderInfo[];
startSession(providerId: string, token: CancellationToken): ChatModel | undefined;
getSession(sessionId: string): IChatModel | undefined;
getOrRestoreSession(sessionId: string): IChatModel | undefined;
loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined;

/**
* Returns whether the request was accepted.
Expand Down
10 changes: 9 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatServiceImpl.ts
Expand Up @@ -19,7 +19,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { ISerializableChatData, ISerializableChatsData, ChatModel, ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ISerializableChatData, ISerializableChatsData, ChatModel, ChatWelcomeMessageModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatProgress, IChatProvider, IChatProviderInfo, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatReplyFollowup, IChatService, IChatUserActionEvent, ISlashCommand, ISlashCommandProvider, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';

Expand Down Expand Up @@ -286,6 +286,10 @@ export class ChatService extends Disposable implements IChatService {
return model;
}

getSession(sessionId: string): IChatModel | undefined {
return this._sessionModels.get(sessionId);
}

getOrRestoreSession(sessionId: string): ChatModel | undefined {
const model = this._sessionModels.get(sessionId);
if (model) {
Expand All @@ -301,6 +305,10 @@ export class ChatService extends Disposable implements IChatService {
return this._startSession(sessionData.providerId, sessionData, CancellationToken.None);
}

loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined {
return this._startSession(data.providerId, data, CancellationToken.None);
}

async sendRequest(sessionId: string, request: string | IChatReplyFollowup): Promise<boolean> {
const messageText = typeof request === 'string' ? request : request.message;
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${messageText.substring(0, 20)}${messageText.length > 20 ? '[...]' : ''}}`);
Expand Down