diff --git a/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCopyActions.ts b/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCopyActions.ts index 080094d2c39ba..b445dc1b7bef2 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCopyActions.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCopyActions.ts @@ -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) { diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget.ts index 26207695164fa..978f347924171 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget.ts @@ -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 >{ - element: item, - collapsed: false, - collapsible: false - }; - }); + const treeItems = (this.viewModel?.getItems() ?? []) + .map(item => { + return >{ + element: item, + collapsed: false, + collapsible: false + }; + }); this.tree.setChildren(null, treeItems, { diffIdentityProvider: { @@ -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 { diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts index 57e6878c1f8e0..be1431db21a87 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts @@ -166,7 +166,7 @@ export interface IInteractiveSessionModel { } export interface ISerializableInteractiveSessionsData { - [providerId: string]: ISerializableInteractiveSessionData[]; + [sessionId: string]: ISerializableInteractiveSessionData; } export interface ISerializableInteractiveSessionRequestData { @@ -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; @@ -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) { @@ -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) { @@ -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, diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts index 4c0b597e1ede5..2c1cf41d18aaa 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts @@ -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'; @@ -110,14 +109,14 @@ export class InteractiveSessionService extends Disposable implements IInteractiv private readonly _providers = new Map(); private readonly _sessionModels = new Map(); private readonly _pendingRequests = new Map>(); - private readonly _unprocessedPersistedSessions: ISerializableInteractiveSessionsData; + private readonly _persistedSessions: ISerializableInteractiveSessionsData; private readonly _hasProvider: IContextKey; private readonly _onDidPerformUserAction = this._register(new Emitter()); public readonly onDidPerformUserAction: Event = 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, @@ -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 { @@ -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 {}; @@ -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'); @@ -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 { const messageText = typeof request === 'string' ? request : request.message; this.trace('sendRequest', `sessionId: ${sessionId}, message: ${messageText.substring(0, 20)}${messageText.length > 20 ? '[...]' : ''}}`); diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts index 3f96a66adc265..f3740e3470626 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts @@ -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'; @@ -31,10 +31,9 @@ export interface IInteractiveSessionViewModel { readonly sessionId: string; readonly onDidDisposeModel: Event; readonly onDidChange: Event; - readonly welcomeMessage: IInteractiveWelcomeMessageViewModel | undefined; readonly requestInProgress: boolean; readonly inputPlaceholder?: string; - getItems(): (IInteractiveRequestViewModel | IInteractiveResponseViewModel)[]; + getItems(): (IInteractiveRequestViewModel | IInteractiveResponseViewModel | IInteractiveWelcomeMessageViewModel)[]; } export interface IInteractiveRequestViewModel { @@ -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; } @@ -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() { @@ -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) { } -}