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

Restore welcome message when restoring chat session #180756

Merged
merged 3 commits into from Apr 24, 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
Expand Up @@ -35,6 +35,7 @@ export function registerInteractiveSessionCopyActions() {
if (widget) {
const viewModel = widget.viewModel;
const sessionAsText = viewModel?.getItems()
.filter((item): item is (IInteractiveRequestViewModel | IInteractiveResponseViewModel) => isRequestVM(item) || isResponseVM(item))
.map(stringifyItem)
.join('\n\n');
if (sessionAsText) {
Expand Down
Expand Up @@ -176,18 +176,14 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive

private onDidChangeItems() {
if (this.tree && this.visible) {
const items: InteractiveTreeItem[] = this.viewModel?.getItems() ?? [];
if (this.viewModel?.welcomeMessage) {
items.unshift(this.viewModel.welcomeMessage);
}

const treeItems = items.map(item => {
return <ITreeElement<InteractiveTreeItem>>{
element: item,
collapsed: false,
collapsible: false
};
});
const treeItems = (this.viewModel?.getItems() ?? [])
.map(item => {
return <ITreeElement<InteractiveTreeItem>>{
element: item,
collapsed: false,
collapsible: false
};
});

this.tree.setChildren(null, treeItems, {
diffIdentityProvider: {
Expand All @@ -202,7 +198,7 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive
}
});

const lastItem = items[items.length - 1];
const lastItem = treeItems[treeItems.length - 1]?.element;
if (lastItem && isResponseVM(lastItem) && lastItem.isComplete) {
this.renderFollowups(lastItem.replyFollowups);
} else {
Expand Down
Expand Up @@ -166,7 +166,7 @@ export interface IInteractiveSessionModel {
}

export interface ISerializableInteractiveSessionsData {
[providerId: string]: ISerializableInteractiveSessionData[];
[sessionId: string]: ISerializableInteractiveSessionData;
}

export interface ISerializableInteractiveSessionRequestData {
Expand All @@ -181,7 +181,7 @@ export interface ISerializableInteractiveSessionRequestData {

export interface ISerializableInteractiveSessionData {
sessionId: string;
// welcomeMessage: string | undefined;
welcomeMessage: (string | IInteractiveSessionReplyFollowup[])[] | undefined;
requests: ISerializableInteractiveSessionRequestData[];
requesterUsername: string;
responderUsername: string;
Expand Down Expand Up @@ -266,6 +266,11 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS
return [];
}

if (obj.welcomeMessage) {
const content = obj.welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item);
this._welcomeMessage = new InteractiveSessionWelcomeMessageModel(content, obj.responderUsername, obj.responderAvatarIconUri && URI.revive(obj.responderAvatarIconUri));
}

return requests.map((raw: ISerializableInteractiveSessionRequestData) => {
const request = new InteractiveRequestModel(raw.message, obj.requesterUsername, obj.requesterAvatarIconUri && URI.revive(obj.requesterAvatarIconUri));
if (raw.response || raw.responseErrorDetails) {
Expand All @@ -281,7 +286,11 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS
}

this._session = session;
this._welcomeMessage = welcomeMessage;
if (!this._welcomeMessage) {
// Could also have loaded the welcome message from persisted data
this._welcomeMessage = welcomeMessage;
}

this._isInitializedDeferred.complete();

if (session.onDidChangeState) {
Expand Down Expand Up @@ -378,7 +387,13 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS
requesterAvatarIconUri: this._session!.requesterAvatarIconUri,
responderUsername: this._session!.responderUsername,
responderAvatarIconUri: this._session!.responderAvatarIconUri,
// welcomeMessage: this._welcomeMessage,
welcomeMessage: this._welcomeMessage?.content.map(c => {
if (Array.isArray(c)) {
return c;
} else {
return c.value;
}
}),
requests: this._requests.map((r): ISerializableInteractiveSessionRequestData => {
return {
providerResponseId: r.response?.providerResponseId,
Expand Down
Expand Up @@ -5,7 +5,6 @@

import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { groupBy } from 'vs/base/common/collections';
import { Emitter, Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator';
Expand Down Expand Up @@ -110,14 +109,14 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
private readonly _providers = new Map<string, IInteractiveProvider>();
private readonly _sessionModels = new Map<string, InteractiveSessionModel>();
private readonly _pendingRequests = new Map<string, CancelablePromise<void>>();
private readonly _unprocessedPersistedSessions: ISerializableInteractiveSessionsData;
private readonly _persistedSessions: ISerializableInteractiveSessionsData;
private readonly _hasProvider: IContextKey<boolean>;

private readonly _onDidPerformUserAction = this._register(new Emitter<IInteractiveSessionUserActionEvent>());
public readonly onDidPerformUserAction: Event<IInteractiveSessionUserActionEvent> = this._onDidPerformUserAction.event;

constructor(
@IStorageService storageService: IStorageService,
@IStorageService private readonly storageService: IStorageService,
@ILogService private readonly logService: ILogService,
@IExtensionService private readonly extensionService: IExtensionService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
Expand All @@ -130,21 +129,25 @@ export class InteractiveSessionService extends Disposable implements IInteractiv

const sessionData = storageService.get(serializedInteractiveSessionKey, StorageScope.WORKSPACE, '');
if (sessionData) {
this._unprocessedPersistedSessions = this.deserializeInteractiveSessions(sessionData);
const countsForLog = Object.keys(this._unprocessedPersistedSessions).map(key => `${key}: ${this._unprocessedPersistedSessions[key].length}`).join(', ');
this.trace('constructor', `Restored persisted sessions: ${countsForLog}`);
this._persistedSessions = this.deserializeInteractiveSessions(sessionData);
const countsForLog = Object.keys(this._persistedSessions).length;
this.trace('constructor', `Restored ${countsForLog} persisted sessions`);
} else {
this._unprocessedPersistedSessions = {};
this._persistedSessions = {};
this.trace('constructor', 'No persisted sessions');
}

this._register(storageService.onWillSaveState(e => {
const allSessions = Array.from(this._sessionModels.values())
.filter(session => session.getRequests().length > 0);
const serialized = JSON.stringify(allSessions);
this.trace('onWillSaveState', `Persisting ${this._sessionModels.size} sessions`);
storageService.store(serializedInteractiveSessionKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE);
}));
this._register(storageService.onWillSaveState(() => this.saveState()));
}

private saveState(): void {
let allSessions: (InteractiveSessionModel | ISerializableInteractiveSessionData)[] = Array.from(this._sessionModels.values())
.filter(session => session.getRequests().length > 0);
allSessions = allSessions.concat(Object.values(this._persistedSessions));
this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`);

const serialized = JSON.stringify(allSessions);
this.storageService.store(serializedInteractiveSessionKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE);
}

notifyUserAction(action: IInteractiveSessionUserActionEvent): void {
Expand Down Expand Up @@ -195,7 +198,11 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
throw new Error('Expected array');
}

return groupBy(arrayOfSessions, item => item.providerId);
const sessions = arrayOfSessions.reduce((acc, session) => {
acc[session.sessionId] = session;
return acc;
}, {} as ISerializableInteractiveSessionsData);
return sessions;
} catch (err) {
this.error('deserializeInteractiveSessions', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`);
return {};
Expand Down Expand Up @@ -243,8 +250,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
if (!session) {
if (sessionHistory) {
// sessionHistory was not used, so store it for later
const providerData = this._unprocessedPersistedSessions[model.providerId];
providerData?.unshift(sessionHistory);
this._persistedSessions[sessionHistory.sessionId] = sessionHistory;
}

this.trace('startSession', 'Provider returned no session');
Expand All @@ -267,29 +273,15 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
return model;
}

const sessionData = this.findPersistedSession(sessionId);
const sessionData = this._persistedSessions[sessionId];
if (!sessionData) {
return undefined;
}

this._unprocessedPersistedSessions[sessionData.providerId] =
this._unprocessedPersistedSessions[sessionData.providerId].filter(item => item.sessionId !== sessionId);
delete this._persistedSessions[sessionId];
return this._startSession(sessionData.providerId, sessionData, CancellationToken.None);
}

private findPersistedSession(sessionId: string): ISerializableInteractiveSessionData | undefined {
// TODO maybe this should just be keyed by sessionId
for (const provider of Object.keys(this._unprocessedPersistedSessions)) {
for (const session of this._unprocessedPersistedSessions[provider]) {
if (session.sessionId === sessionId) {
return session;
}
}
}

return undefined;
}

async sendRequest(sessionId: string, request: string | IInteractiveSessionReplyFollowup): 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
Expand Up @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IInteractiveRequestModel, IInteractiveResponseModel, IInteractiveSessionModel, IInteractiveSessionWelcomeMessageModel, IInteractiveWelcomeMessageContent } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
import { IInteractiveRequestModel, IInteractiveResponseModel, IInteractiveSessionModel, IInteractiveWelcomeMessageContent } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
import { IInteractiveResponseErrorDetails, IInteractiveSessionReplyFollowup, IInteractiveSessionResponseCommandFollowup, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { countWords } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter';

Expand All @@ -31,10 +31,9 @@ export interface IInteractiveSessionViewModel {
readonly sessionId: string;
readonly onDidDisposeModel: Event<void>;
readonly onDidChange: Event<void>;
readonly welcomeMessage: IInteractiveWelcomeMessageViewModel | undefined;
readonly requestInProgress: boolean;
readonly inputPlaceholder?: string;
getItems(): (IInteractiveRequestViewModel | IInteractiveResponseViewModel)[];
getItems(): (IInteractiveRequestViewModel | IInteractiveResponseViewModel | IInteractiveWelcomeMessageViewModel)[];
}

export interface IInteractiveRequestViewModel {
Expand Down Expand Up @@ -93,10 +92,6 @@ export class InteractiveSessionViewModel extends Disposable implements IInteract
return this._model.inputPlaceholder;
}

get welcomeMessage() {
return this._model.welcomeMessage;
}

get sessionId() {
return this._model.sessionId;
}
Expand Down Expand Up @@ -147,7 +142,7 @@ export class InteractiveSessionViewModel extends Disposable implements IInteract
}

getItems() {
return [...this._items];
return [...(this._model.welcomeMessage ? [this._model.welcomeMessage] : []), ...this._items];
}

override dispose() {
Expand Down Expand Up @@ -326,23 +321,3 @@ export interface IInteractiveWelcomeMessageViewModel {
readonly avatarIconUri?: URI;
readonly content: IInteractiveWelcomeMessageContent[];
}

export class InteractiveWelcomeMessageViewModel implements IInteractiveWelcomeMessageViewModel {
get id() {
return this._model.id;
}

get username() {
return this._model.username;
}

get avatarIconUri() {
return this._model.avatarIconUri;
}

get content() {
return this._model.content;
}

constructor(readonly _model: IInteractiveSessionWelcomeMessageModel) { }
}