Skip to content

Commit fd9cf8e

Browse files
authored
chat: refactor input state to be central (#278683)
* chat: add additional timing information on the model - Add a public created `timestamp` on the IChatModel - Add a started `timestamp` and optional `completedAt` timestamp on the IChatResponseModel - Make `isPendingConfirmation<{startedWaitingAt: number}> | undefined` to encode the time when the response started waiting for confirmation. - Add a `confirmationAdjustedTimestamp` that can be used to reflect the duration a chat response was waiting for user input vs working. * update snapshots * wip * fix mode picker changing wrong input when focused * wip * make dynamic variables happy * tidy
1 parent 8754da8 commit fd9cf8e

File tree

18 files changed

+537
-299
lines changed

18 files changed

+537
-299
lines changed

src/vs/workbench/api/browser/mainThreadChatAgents2.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,16 +147,14 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
147147

148148
$transferActiveChatSession(toWorkspace: UriComponents): void {
149149
const widget = this._chatWidgetService.lastFocusedWidget;
150-
const sessionId = widget?.viewModel?.model.sessionId;
151-
if (!sessionId) {
150+
const model = widget?.viewModel?.model;
151+
if (!model) {
152152
this._logService.error(`MainThreadChat#$transferActiveChatSession: No active chat session found`);
153153
return;
154154
}
155155

156-
const inputValue = widget?.inputEditor.getValue() ?? '';
157156
const location = widget.location;
158-
const mode = widget.input.currentModeKind;
159-
this._chatService.transferChatSession({ sessionId, inputValue, location, mode }, URI.revive(toWorkspace));
157+
this._chatService.transferChatSession({ sessionId: model.sessionId, inputState: model.inputModel.state.get(), location }, URI.revive(toWorkspace));
160158
}
161159

162160
async $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): Promise<void> {

src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
88
import { basename } from '../../../../../base/common/resources.js';
99
import { ThemeIcon } from '../../../../../base/common/themables.js';
1010
import { assertType } from '../../../../../base/common/types.js';
11+
import { URI } from '../../../../../base/common/uri.js';
1112
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
1213
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
1314
import { localize, localize2 } from '../../../../../nls.js';
@@ -273,6 +274,7 @@ export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode';
273274

274275
export interface IToggleChatModeArgs {
275276
modeId: ChatModeKind | string;
277+
sessionResource: URI | undefined;
276278
}
277279

278280
type ChatModeChangeClassification = {
@@ -319,23 +321,30 @@ class ToggleChatModeAction extends Action2 {
319321
const instaService = accessor.get(IInstantiationService);
320322
const modeService = accessor.get(IChatModeService);
321323
const telemetryService = accessor.get(ITelemetryService);
324+
const chatWidgetService = accessor.get(IChatWidgetService);
322325

323-
const context = getEditingSessionContext(accessor, args);
324-
if (!context?.chatWidget) {
326+
const arg = args.at(0) as IToggleChatModeArgs | undefined;
327+
let widget: IChatWidget | undefined;
328+
if (arg?.sessionResource) {
329+
widget = chatWidgetService.getWidgetBySessionResource(arg.sessionResource);
330+
} else {
331+
widget = getEditingSessionContext(accessor, args)?.chatWidget;
332+
}
333+
334+
if (!widget) {
325335
return;
326336
}
327337

328-
const arg = args.at(0) as IToggleChatModeArgs | undefined;
329-
const chatSession = context.chatWidget.viewModel?.model;
338+
const chatSession = widget.viewModel?.model;
330339
const requestCount = chatSession?.getRequests().length ?? 0;
331-
const switchToMode = (arg && modeService.findModeById(arg.modeId)) ?? this.getNextMode(context.chatWidget, requestCount, configurationService, modeService);
340+
const switchToMode = (arg && modeService.findModeById(arg.modeId)) ?? this.getNextMode(widget, requestCount, configurationService, modeService);
332341

333-
const currentMode = context.chatWidget.input.currentModeObs.get();
342+
const currentMode = widget.input.currentModeObs.get();
334343
if (switchToMode.id === currentMode.id) {
335344
return;
336345
}
337346

338-
const chatModeCheck = await instaService.invokeFunction(handleModeSwitch, context.chatWidget.input.currentModeKind, switchToMode.kind, requestCount, context.editingSession);
347+
const chatModeCheck = await instaService.invokeFunction(handleModeSwitch, widget.input.currentModeKind, switchToMode.kind, requestCount, widget.viewModel?.model.editingSession);
339348
if (!chatModeCheck) {
340349
return;
341350
}
@@ -356,7 +365,7 @@ class ToggleChatModeAction extends Action2 {
356365
handoffsCount
357366
});
358367

359-
context.chatWidget.input.setChatMode(switchToMode.id);
368+
widget.input.setChatMode(switchToMode.id);
360369

361370
if (chatModeCheck.needToClearSession) {
362371
await commandService.executeCommand(ACTION_ID_NEW_CHAT);

src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,15 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew
131131
return;
132132
}
133133

134-
// Save off the state before clearing
135-
const viewState = widget.getViewState();
134+
// Save off the session resource before clearing
136135
const resourceToOpen = widget.viewModel.sessionResource;
137136

137+
// Todo: can possibly go away with https://github.com/microsoft/vscode/pull/278476
138+
const modelInputState = existingWidget.getViewState();
139+
138140
await widget.clear();
139141

140-
const options: IChatEditorOptions = { pinned: true, viewState, auxiliary };
142+
const options: IChatEditorOptions = { pinned: true, modelInputState, auxiliary };
141143
await editorService.openEditor({ resource: resourceToOpen, options }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP);
142144
}
143145

@@ -150,9 +152,15 @@ async function moveToSidebar(accessor: ServicesAccessor): Promise<void> {
150152
const chatEditorInput = chatEditor?.input;
151153
let view: ChatViewPane;
152154
if (chatEditor instanceof ChatEditor && chatEditorInput instanceof ChatEditorInput && chatEditorInput.sessionResource) {
155+
const previousViewState = chatEditor.widget.getViewState();
153156
await editorService.closeEditor({ editor: chatEditor.input, groupId: editorGroupService.activeGroup.id });
154157
view = await viewsService.openView(ChatViewId) as ChatViewPane;
155-
await view.loadSession(chatEditorInput.sessionResource, chatEditor.getViewState());
158+
159+
// Todo: can possibly go away with https://github.com/microsoft/vscode/pull/278476
160+
const newModel = await view.loadSession(chatEditorInput.sessionResource);
161+
if (previousViewState && newModel && !newModel.inputModel.state.get()) {
162+
newModel.inputModel.setState(previousViewState);
163+
}
156164
} else {
157165
view = await viewsService.openView(ChatViewId) as ChatViewPane;
158166
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte
1515
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
1616
import { PreferredGroup } from '../../../services/editor/common/editorService.js';
1717
import { IChatAgentAttachmentCapabilities, IChatAgentCommand, IChatAgentData } from '../common/chatAgents.js';
18-
import { IChatResponseModel } from '../common/chatModel.js';
18+
import { IChatResponseModel, IChatModelInputState } from '../common/chatModel.js';
1919
import { IChatMode } from '../common/chatModes.js';
2020
import { IParsedChatRequest } from '../common/chatParserTypes.js';
2121
import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js';
@@ -25,7 +25,7 @@ import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
2525
import { ChatAttachmentModel } from './chatAttachmentModel.js';
2626
import { IChatEditorOptions } from './chatEditor.js';
2727
import { ChatInputPart } from './chatInputPart.js';
28-
import { ChatWidget, IChatViewState, IChatWidgetContrib } from './chatWidget.js';
28+
import { ChatWidget, IChatWidgetContrib } from './chatWidget.js';
2929
import { ICodeBlockActionContext } from './codeBlockPart.js';
3030

3131
export const IChatWidgetService = createDecorator<IChatWidgetService>('chatWidgetService');
@@ -223,6 +223,7 @@ export interface IChatWidget {
223223
readonly input: ChatInputPart;
224224
readonly attachmentModel: ChatAttachmentModel;
225225
readonly locationData?: IChatLocationData;
226+
readonly contribs: readonly IChatWidgetContrib[];
226227

227228
readonly supportsChangingModes: boolean;
228229

@@ -254,7 +255,7 @@ export interface IChatWidget {
254255
getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[];
255256
getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined;
256257
clear(): Promise<void>;
257-
getViewState(): IChatViewState;
258+
getViewState(): IChatModelInputState | undefined;
258259
lockToCodingAgent(name: string, displayName: string, agentId?: string): void;
259260

260261
delegateScrollFromMouseWheelEvent(event: IMouseWheelEvent): void;

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

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,30 @@ import { IContextKeyService, IScopedContextKeyService } from '../../../../platfo
1414
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
1515
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
1616
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
17-
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
17+
import { IStorageService } from '../../../../platform/storage/common/storage.js';
1818
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
1919
import { editorBackground, editorForeground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js';
2020
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
2121
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
2222
import { IEditorOpenContext } from '../../../common/editor.js';
23-
import { Memento } from '../../../common/memento.js';
2423
import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js';
2524
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
2625
import { ChatContextKeys } from '../common/chatContextKeys.js';
27-
import { IChatModel, IExportableChatData, ISerializableChatData } from '../common/chatModel.js';
28-
import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js';
26+
import { IChatModel, IChatModelInputState, IExportableChatData, ISerializableChatData } from '../common/chatModel.js';
2927
import { IChatService } from '../common/chatService.js';
3028
import { IChatSessionsService, localChatSessionType } from '../common/chatSessionsService.js';
3129
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
3230
import { clearChatEditor } from './actions/chatClear.js';
3331
import { ChatEditorInput } from './chatEditorInput.js';
34-
import { ChatWidget, IChatViewState } from './chatWidget.js';
32+
import { ChatWidget } from './chatWidget.js';
3533

3634
export interface IChatEditorOptions extends IEditorOptions {
35+
/**
36+
* Input state of the model when the editor is opened. Currently needed since
37+
* new sessions are not persisted but may go away with
38+
* https://github.com/microsoft/vscode/pull/278476 as input state is stored on the model.
39+
*/
40+
modelInputState?: IChatModelInputState;
3741
target?: { data: IExportableChatData | ISerializableChatData };
3842
title?: {
3943
preferred?: string;
@@ -52,8 +56,6 @@ export class ChatEditor extends EditorPane {
5256
return this._scopedContextKeyService;
5357
}
5458

55-
private _memento: Memento<IChatViewState> | undefined;
56-
private _viewState: IChatViewState | undefined;
5759
private dimension = new dom.Dimension(0, 0);
5860
private _loadingContainer: HTMLElement | undefined;
5961
private _editorContainer: HTMLElement | undefined;
@@ -63,7 +65,7 @@ export class ChatEditor extends EditorPane {
6365
@ITelemetryService telemetryService: ITelemetryService,
6466
@IThemeService themeService: IThemeService,
6567
@IInstantiationService private readonly instantiationService: IInstantiationService,
66-
@IStorageService private readonly storageService: IStorageService,
68+
@IStorageService storageService: IStorageService,
6769
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
6870
@IContextKeyService private readonly contextKeyService: IContextKeyService,
6971
@IChatService private readonly chatService: IChatService,
@@ -231,8 +233,11 @@ export class ChatEditor extends EditorPane {
231233
this.hideLoadingInChatWidget();
232234
}
233235

234-
const viewState = options?.viewState ?? input.options.viewState;
235-
this.updateModel(editorModel.model, viewState);
236+
if (options?.modelInputState) {
237+
editorModel.model.inputModel.setState(options.modelInputState);
238+
}
239+
240+
this.updateModel(editorModel.model);
236241

237242
if (isContributedChatSession && options?.title?.preferred && input.sessionResource) {
238243
this.chatService.setChatSessionTitle(input.sessionResource, options.title.preferred);
@@ -243,27 +248,8 @@ export class ChatEditor extends EditorPane {
243248
}
244249
}
245250

246-
private updateModel(model: IChatModel, viewState?: IChatViewState): void {
247-
this._memento = new Memento('interactive-session-editor-' + CHAT_PROVIDER_ID, this.storageService);
248-
this._viewState = viewState ?? this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);
249-
this.widget.setModel(model, { ...this._viewState });
250-
}
251-
252-
protected override saveState(): void {
253-
this.widget?.saveState();
254-
255-
if (this._memento && this._viewState) {
256-
const widgetViewState = this.widget.getViewState();
257-
258-
// Need to set props individually on the memento
259-
this._viewState.inputValue = widgetViewState.inputValue;
260-
this._viewState.inputState = widgetViewState.inputState;
261-
this._memento.saveMemento();
262-
}
263-
}
264-
265-
override getViewState(): object | undefined {
266-
return { ...this._viewState };
251+
private updateModel(model: IChatModel): void {
252+
this.widget.setModel(model);
267253
}
268254

269255
override layout(dimension: dom.Dimension, position?: dom.IDomPosition | undefined): void {

0 commit comments

Comments
 (0)