diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index d9bf9d1fe5a..c07de21aed8 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -2311,6 +2311,17 @@ export class ChatModel extends Disposable implements IChatModel { modelState = { value: ResponseModelState.Cancelled, completedAt: Date.now() }; } + // Mark question carousels as used after + // deserialization. After a reload, the extension is no longer listening for + // their responses, so they cannot be interacted with. + if (raw.response) { + for (const part of raw.response) { + if (hasKey(part, { kind: true }) && (part.kind === 'questionCarousel')) { + part.isUsed = true; + } + } + } + request.response = new ChatResponseModel({ responseContent: raw.response ?? [new MarkdownString(raw.response)], session: this, diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts index 934d86152e6..c0800d4b46d 100644 --- a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts @@ -167,6 +167,51 @@ suite('ChatModel', () => { assert.strictEqual(request1.response!.shouldBeRemovedOnSend, undefined); }); + test('deserialization marks unused question carousels as used', async () => { + const serializableData: ISerializableChatData3 = { + version: 3, + sessionId: 'test-session', + creationDate: Date.now(), + customTitle: undefined, + initialLocation: ChatAgentLocation.Chat, + requests: [{ + requestId: 'req1', + message: { text: 'hello', parts: [] }, + variableData: { variables: [] }, + response: [ + { value: 'some text', isTrusted: false }, + { + kind: 'questionCarousel' as const, + questions: [{ id: 'q1', title: 'Question 1', type: 'text' as const }], + allowSkip: true, + resolveId: 'resolve1', + isUsed: false, + }, + ], + modelState: { value: 2 /* ResponseModelState.Cancelled */, completedAt: Date.now() }, + }], + responderUsername: 'bot', + }; + + const model = testDisposables.add(instantiationService.createInstance( + ChatModel, + { value: serializableData, serializer: undefined! }, + { initialLocation: ChatAgentLocation.Chat, canUseTools: true } + )); + + const requests = model.getRequests(); + assert.strictEqual(requests.length, 1); + const response = requests[0].response!; + + // The question carousel should be marked as used after deserialization + const carouselPart = response.response.value.find(p => p.kind === 'questionCarousel'); + assert.ok(carouselPart); + assert.strictEqual(carouselPart.isUsed, true); + + // The response should be complete (not stuck in NeedsInput) + assert.strictEqual(response.isComplete, true); + }); + test('inputModel.toJSON filters extension-contributed contexts', async function () { const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true }));