Skip to content

Commit 3196eb9

Browse files
authored
Enable chat session export/import (microsoft#182562)
1 parent 3aec07b commit 3196eb9

File tree

7 files changed

+147
-13
lines changed

7 files changed

+147
-13
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { VSBuffer } from 'vs/base/common/buffer';
7+
import { joinPath } from 'vs/base/common/resources';
8+
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
9+
import { localize } from 'vs/nls';
10+
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
11+
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
12+
import { IFileService } from 'vs/platform/files/common/files';
13+
import { INTERACTIVE_SESSION_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
14+
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
15+
import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
16+
import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
17+
import { isSerializableSessionData } from 'vs/workbench/contrib/chat/common/chatModel';
18+
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
19+
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
20+
21+
const defaultFileName = 'chat.json';
22+
const filters = [{ name: localize('interactiveSession.file.label', "Chat Session"), extensions: ['json'] }];
23+
24+
export function registerInteractiveSessionExportActions() {
25+
registerAction2(class ExportInteractiveSessionAction extends Action2 {
26+
constructor() {
27+
super({
28+
id: 'workbench.action.interactiveSession.export',
29+
category: INTERACTIVE_SESSION_CATEGORY,
30+
title: {
31+
value: localize('interactiveSession.export.label', "Export Session") + '...',
32+
original: 'Export Session...'
33+
},
34+
f1: true,
35+
});
36+
}
37+
async run(accessor: ServicesAccessor, ...args: any[]) {
38+
const widgetService = accessor.get(IChatWidgetService);
39+
const fileDialogService = accessor.get(IFileDialogService);
40+
const fileService = accessor.get(IFileService);
41+
const interactiveSessionService = accessor.get(IChatService);
42+
43+
const widget = widgetService.lastFocusedWidget;
44+
if (!widget || !widget.viewModel) {
45+
return;
46+
}
47+
48+
const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultFileName);
49+
const result = await fileDialogService.showSaveDialog({
50+
defaultUri,
51+
filters
52+
});
53+
if (!result) {
54+
return;
55+
}
56+
57+
const model = interactiveSessionService.getSession(widget.viewModel.sessionId);
58+
if (!model) {
59+
return;
60+
}
61+
62+
// Using toJSON on the model
63+
const content = VSBuffer.fromString(JSON.stringify(model, undefined, 2));
64+
await fileService.writeFile(result, content);
65+
}
66+
});
67+
68+
registerAction2(class ImportInteractiveSessionAction extends Action2 {
69+
constructor() {
70+
super({
71+
id: 'workbench.action.interactiveSession.import',
72+
title: {
73+
value: localize('interactiveSession.import.label', "Import Session") + '...',
74+
original: 'Export Session...'
75+
},
76+
category: INTERACTIVE_SESSION_CATEGORY,
77+
f1: true,
78+
});
79+
}
80+
async run(accessor: ServicesAccessor, ...args: any[]) {
81+
const fileDialogService = accessor.get(IFileDialogService);
82+
const fileService = accessor.get(IFileService);
83+
const editorService = accessor.get(IEditorService);
84+
85+
const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultFileName);
86+
const result = await fileDialogService.showOpenDialog({
87+
defaultUri,
88+
canSelectFiles: true,
89+
filters
90+
});
91+
if (!result) {
92+
return;
93+
}
94+
95+
const content = await fileService.readFile(result[0]);
96+
try {
97+
const data = JSON.parse(content.value.toString());
98+
if (!isSerializableSessionData(data)) {
99+
throw new Error('Invalid chat session data');
100+
}
101+
102+
await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: <IChatEditorOptions>{ target: { data }, pinned: true } });
103+
} catch (err) {
104+
throw err;
105+
}
106+
}
107+
});
108+
}

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { Disposable } from 'vs/base/common/lifecycle';
7+
import { Schemas } from 'vs/base/common/network';
78
import { isMacintosh } from 'vs/base/common/platform';
89
import * as nls from 'vs/nls';
910
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
@@ -18,17 +19,19 @@ import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/c
1819
import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions';
1920
import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions';
2021
import { registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
21-
import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions';
2222
import { registerChatQuickQuestionActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
23+
import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions';
24+
import { registerInteractiveSessionExportActions } from 'vs/workbench/contrib/chat/browser/actions/interactiveSessionImportExport';
2325
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
2426
import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl';
25-
import { IChatEditorOptions, ChatEditor } from 'vs/workbench/contrib/chat/browser/chatEditor';
27+
import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
2628
import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
2729
import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget';
30+
import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib';
2831
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
2932
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
3033
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
31-
import { IChatWidgetHistoryService, ChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService';
34+
import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService';
3235
import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService';
3336
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
3437
import '../common/chatColors';
@@ -124,11 +127,10 @@ registerChatCodeBlockActions();
124127
registerChatTitleActions();
125128
registerChatExecuteActions();
126129
registerChatQuickQuestionActions();
130+
registerInteractiveSessionExportActions();
127131

128132
registerSingleton(IChatService, ChatService, InstantiationType.Delayed);
129133
registerSingleton(IChatContributionService, ChatContributionService, InstantiationType.Delayed);
130134
registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed);
131135
registerSingleton(IChatWidgetHistoryService, ChatWidgetHistoryService, InstantiationType.Delayed);
132136

133-
import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib';
134-
import { Schemas } from 'vs/base/common/network';

src/vs/workbench/contrib/chat/browser/chatEditor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import { Memento } from 'vs/workbench/common/memento';
1919
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
2020
import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
2121
import { IViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
22-
import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
22+
import { IChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
2323
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
2424

2525
export interface IChatEditorOptions extends IEditorOptions {
26-
target: { sessionId: string } | { providerId: string };
26+
target: { sessionId: string } | { providerId: string } | { data: ISerializableChatData };
2727
}
2828

2929
export class ChatEditor extends EditorPane {

src/vs/workbench/contrib/chat/browser/chatEditorInput.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,14 @@ export class ChatEditorInput extends EditorInput {
6969
}
7070

7171
override async resolve(): Promise<ChatEditorModel | null> {
72-
const model = typeof this.sessionId === 'string' ?
73-
this.chatService.getOrRestoreSession(this.sessionId) :
74-
this.chatService.startSession(this.providerId!, CancellationToken.None);
72+
let model: IChatModel | undefined;
73+
if (typeof this.sessionId === 'string') {
74+
model = this.chatService.getOrRestoreSession(this.sessionId);
75+
} else if (typeof this.providerId === 'string') {
76+
model = this.chatService.startSession(this.providerId, CancellationToken.None);
77+
} else if ('data' in this.options.target) {
78+
model = this.chatService.loadSessionFromContent(this.options.target.data);
79+
}
7580

7681
if (!model) {
7782
return null;

src/vs/workbench/contrib/chat/common/chatModel.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,26 @@ export interface ISerializableChatRequestData {
198198

199199
export interface ISerializableChatData {
200200
sessionId: string;
201+
providerId: string;
201202
creationDate: number;
202203
welcomeMessage: (string | IChatReplyFollowup[])[] | undefined;
203204
requests: ISerializableChatRequestData[];
204205
requesterUsername: string;
205206
responderUsername: string;
206207
requesterAvatarIconUri: UriComponents | undefined;
207208
responderAvatarIconUri: UriComponents | undefined;
208-
providerId: string;
209209
providerState: any;
210210
}
211211

212+
export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData {
213+
const data = obj as ISerializableChatData;
214+
return typeof data === 'object' &&
215+
typeof data.providerId === 'string' &&
216+
typeof data.sessionId === 'string' &&
217+
typeof data.requesterUsername === 'string' &&
218+
typeof data.responderUsername === 'string';
219+
}
220+
212221
export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent;
213222

214223
export interface IChatAddRequestEvent {

src/vs/workbench/contrib/chat/common/chatService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
99
import { URI } from 'vs/base/common/uri';
1010
import { ProviderResult } from 'vs/editor/common/languages';
1111
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
12-
import { IChatModel, ChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
12+
import { IChatModel, ChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
1313

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

182184
/**
183185
* Returns whether the request was accepted.

src/vs/workbench/contrib/chat/common/chatServiceImpl.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ILogService } from 'vs/platform/log/common/log';
1919
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
2020
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2121
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
22-
import { ISerializableChatData, ISerializableChatsData, ChatModel, ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel';
22+
import { ISerializableChatData, ISerializableChatsData, ChatModel, ChatWelcomeMessageModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
2323
import { IChatProgress, IChatProvider, IChatProviderInfo, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatReplyFollowup, IChatService, IChatUserActionEvent, ISlashCommand, ISlashCommandProvider, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
2424
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
2525

@@ -286,6 +286,10 @@ export class ChatService extends Disposable implements IChatService {
286286
return model;
287287
}
288288

289+
getSession(sessionId: string): IChatModel | undefined {
290+
return this._sessionModels.get(sessionId);
291+
}
292+
289293
getOrRestoreSession(sessionId: string): ChatModel | undefined {
290294
const model = this._sessionModels.get(sessionId);
291295
if (model) {
@@ -301,6 +305,10 @@ export class ChatService extends Disposable implements IChatService {
301305
return this._startSession(sessionData.providerId, sessionData, CancellationToken.None);
302306
}
303307

308+
loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined {
309+
return this._startSession(data.providerId, data, CancellationToken.None);
310+
}
311+
304312
async sendRequest(sessionId: string, request: string | IChatReplyFollowup): Promise<boolean> {
305313
const messageText = typeof request === 'string' ? request : request.message;
306314
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${messageText.substring(0, 20)}${messageText.length > 20 ? '[...]' : ''}}`);

0 commit comments

Comments
 (0)