Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/linkify/common/responseStreamWithLinkification.ts
13399 views
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
import type { ChatQuestion, ChatResponseClearToPreviousToolInvocationReason, ChatResponseFileTree, ChatResponsePart, ChatResponseStream, ChatResultUsage, ChatToolInvocationStreamData, ChatVulnerability, ChatWorkspaceFileEdit, Command, Location, NotebookEdit, TextEdit, ThinkingDelta, Uri } from 'vscode';
6
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
7
import { FinalizableChatResponseStream } from '../../../util/common/chatResponseStreamImpl';
8
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
9
import { ChatHookType, ChatResponseAnchorPart, ChatResponseCommandButtonPart, ChatResponseConfirmationPart, ChatResponseFileTreePart, ChatResponseMarkdownPart, ChatResponseThinkingProgressPart, ChatToolInvocationPart, MarkdownString } from '../../../vscodeTypes';
10
import { LinkifiedText, LinkifySymbolAnchor } from './linkifiedText';
11
import { IContributedLinkifierFactory, ILinkifier, ILinkifyService, LinkifierContext } from './linkifyService';
12
13
/**
14
* Proxy of {@linkcode ChatResponseStream} that linkifies paths and symbols in emitted Markdown.
15
*/
16
export class ResponseStreamWithLinkification implements FinalizableChatResponseStream {
17
18
private readonly _linkifier: ILinkifier;
19
private readonly _progress: ChatResponseStream;
20
private readonly _token: CancellationToken;
21
22
constructor(
23
context: LinkifierContext,
24
progress: ChatResponseStream,
25
additionalLinkifiers: readonly IContributedLinkifierFactory[],
26
token: CancellationToken,
27
@ILinkifyService linkifyService: ILinkifyService,
28
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
29
) {
30
this._linkifier = linkifyService.createLinkifier(context, additionalLinkifiers);
31
this._progress = progress;
32
this._token = token;
33
}
34
35
get totalAddedLinkCount() {
36
return this._linkifier.totalAddedLinkCount;
37
}
38
39
clearToPreviousToolInvocation(reason: ChatResponseClearToPreviousToolInvocationReason): void {
40
this._pendingMarkdown = '';
41
this._pendingMarkdownScheduled = false;
42
this._linkifier.flush(CancellationToken.None);
43
this._progress.clearToPreviousToolInvocation(reason);
44
}
45
46
//#region ChatResponseStream
47
markdown(value: string | MarkdownString): ChatResponseStream {
48
this.appendMarkdown(typeof value === 'string' ? new MarkdownString(value) : value);
49
return this;
50
}
51
52
anchor(value: Uri | Location, title?: string | undefined): ChatResponseStream {
53
this.enqueue(() => this._progress.anchor(value, title), false);
54
return this;
55
}
56
57
button(command: Command): ChatResponseStream {
58
this.enqueue(() => this._progress.button(command), true);
59
return this;
60
}
61
62
filetree(value: ChatResponseFileTree[], baseUri: Uri): ChatResponseStream {
63
this.enqueue(() => this._progress.filetree(value, baseUri), true);
64
return this;
65
}
66
67
progress(value: string): ChatResponseStream {
68
this.enqueue(() => this._progress.progress(value), false);
69
return this;
70
}
71
72
thinkingProgress(thinkingDelta: ThinkingDelta): ChatResponseStream {
73
this.enqueue(() => this._progress.thinkingProgress(thinkingDelta), false);
74
return this;
75
}
76
77
warning(value: string | MarkdownString): ChatResponseStream {
78
this.enqueue(() => this._progress.warning(value), false);
79
return this;
80
}
81
82
info(value: string | MarkdownString): ChatResponseStream {
83
this.enqueue(() => this._progress.info(value), false);
84
return this;
85
}
86
87
hookProgress(hookType: ChatHookType, stopReason?: string, systemMessage?: string): ChatResponseStream {
88
this.enqueue(() => this._progress.hookProgress(hookType, stopReason, systemMessage), false);
89
return this;
90
}
91
92
93
reference(value: Uri | Location): ChatResponseStream {
94
this.enqueue(() => this._progress.reference(value), false);
95
return this;
96
}
97
98
reference2(value: Uri | Location): ChatResponseStream {
99
this.enqueue(() => this._progress.reference(value), false);
100
return this;
101
}
102
103
codeCitation(value: Uri, license: string, snippet: string): ChatResponseStream {
104
this.enqueue(() => this._progress.codeCitation(value, license, snippet), false);
105
return this;
106
}
107
108
externalEdit(target: Uri | Uri[], callback: () => Thenable<void>): Thenable<string> {
109
return this.enqueue(() => this._progress.externalEdit(target, callback), true);
110
}
111
112
push(part: ChatResponsePart): ChatResponseStream {
113
if (part instanceof ChatResponseMarkdownPart) {
114
this.appendMarkdown(part.value);
115
} else {
116
this.enqueue(() => this._progress.push(part), this.isBlockPart(part));
117
}
118
return this;
119
}
120
121
private isBlockPart(part: ChatResponsePart): boolean {
122
return part instanceof ChatResponseFileTreePart
123
|| part instanceof ChatResponseCommandButtonPart
124
|| part instanceof ChatResponseConfirmationPart
125
|| part instanceof ChatToolInvocationPart
126
|| part instanceof ChatResponseThinkingProgressPart;
127
}
128
129
textEdit(target: Uri, editsOrDone: TextEdit | TextEdit[] | true): ChatResponseStream {
130
// TS makes me do this
131
if (editsOrDone === true) {
132
this.enqueue(() => this._progress.textEdit(target, editsOrDone), false);
133
} else {
134
this.enqueue(() => this._progress.textEdit(target, editsOrDone), false);
135
}
136
137
return this;
138
}
139
140
notebookEdit(target: Uri, edits: NotebookEdit | NotebookEdit[]): void;
141
notebookEdit(target: Uri, isDone: true): void;
142
notebookEdit(target: Uri, editsOrDone: NotebookEdit | NotebookEdit[] | true): ChatResponseStream {
143
// TS makes me do this
144
if (editsOrDone === true) {
145
this.enqueue(() => this._progress.notebookEdit(target, editsOrDone), false);
146
} else {
147
this.enqueue(() => this._progress.notebookEdit(target, editsOrDone), false);
148
}
149
return this;
150
}
151
152
workspaceEdit(edits: ChatWorkspaceFileEdit[]): void {
153
this.enqueue(() => this._progress.workspaceEdit(edits), false);
154
}
155
156
markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): ChatResponseStream {
157
this.enqueue(() => this._progress.markdownWithVulnerabilities(value, vulnerabilities), false);
158
return this;
159
}
160
161
codeblockUri(uri: Uri, isEdit?: boolean): void {
162
if ('codeblockUri' in this._progress) {
163
this.enqueue(() => this._progress.codeblockUri(uri, isEdit), false);
164
}
165
}
166
167
confirmation(title: string, message: string, data: any): ChatResponseStream {
168
this.enqueue(() => this._progress.confirmation(title, message, data), true);
169
return this;
170
}
171
172
beginToolInvocation(toolCallId: string, toolName: string, streamData?: ChatToolInvocationStreamData): ChatResponseStream {
173
this.enqueue(() => this._progress.beginToolInvocation(toolCallId, toolName, streamData), true);
174
return this;
175
}
176
177
updateToolInvocation(toolCallId: string, streamData: { partialInput?: unknown }): ChatResponseStream {
178
this.enqueue(() => this._progress.updateToolInvocation(toolCallId, streamData), false);
179
return this;
180
}
181
182
questionCarousel(questions: ChatQuestion[], allowSkip?: boolean): Thenable<Record<string, unknown> | undefined> {
183
return this.enqueue(() => this._progress.questionCarousel(questions, allowSkip), true);
184
}
185
186
usage(usage: ChatResultUsage): ChatResponseStream {
187
this.enqueue(() => this._progress.usage(usage), false);
188
return this;
189
}
190
191
//#endregion
192
193
private sequencer: Promise<unknown> = Promise.resolve();
194
195
private enqueue<T>(f: () => T | Thenable<T>, flush: boolean) {
196
if (flush) {
197
this.sequencer = this.sequencer.then(() => this.doFinalize());
198
}
199
this.sequencer = this.sequencer.then(f);
200
return this.sequencer as Promise<T>;
201
}
202
203
private _pendingMarkdown = '';
204
private _pendingMarkdownScheduled = false;
205
206
private async appendMarkdown(md: MarkdownString): Promise<void> {
207
if (!md.value) {
208
return;
209
}
210
211
// Buffer incoming markdown and schedule a single drain when the sequencer frees up.
212
// This coalesces many small markdown chunks into fewer linkifier.append() calls,
213
// dramatically reducing queue wait when the linkifier is busy.
214
this._pendingMarkdown += md.value;
215
216
if (!this._pendingMarkdownScheduled) {
217
this._pendingMarkdownScheduled = true;
218
this.enqueue(async () => {
219
const buf = this._pendingMarkdown;
220
this._pendingMarkdown = '';
221
this._pendingMarkdownScheduled = false;
222
223
const output = await this._linkifier.append(buf, this._token);
224
if (this._token.isCancellationRequested) {
225
return;
226
}
227
228
this.outputMarkdown(output);
229
}, false);
230
}
231
}
232
233
async finalize() {
234
await this.enqueue(() => this.doFinalize(), false);
235
}
236
237
private async doFinalize() {
238
const textToApply = await this._linkifier.flush(this._token);
239
if (this._token.isCancellationRequested) {
240
return;
241
}
242
243
if (textToApply) {
244
this.outputMarkdown(textToApply);
245
}
246
}
247
248
private outputMarkdown(textToApply: LinkifiedText) {
249
for (const part of textToApply.parts) {
250
if (typeof part === 'string') {
251
if (!part.length) {
252
continue;
253
}
254
255
const content = new MarkdownString(part);
256
257
const folder = this.workspaceService.getWorkspaceFolders()?.at(0);
258
if (folder) {
259
content.baseUri = folder.path.endsWith('/') ? folder : folder.with({ path: folder.path + '/' });
260
}
261
262
this._progress.markdown(content);
263
} else {
264
if (part instanceof LinkifySymbolAnchor) {
265
const chatPart = new ChatResponseAnchorPart(part.symbolInformation as any);
266
if (part.resolve) {
267
(chatPart as any).resolve = () => part.resolve!(this._token);
268
}
269
this._progress.push(chatPart);
270
} else {
271
this._progress.anchor(part.value, part.title);
272
}
273
}
274
}
275
}
276
}
277
278