Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts
13400 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
6
import { Raw } from '@vscode/prompt-tsx';
7
import type { InlineCompletionContext } from 'vscode';
8
import * as yaml from 'yaml';
9
import { ErrorUtils } from '../../../util/common/errors';
10
import { isCancellationError } from '../../../util/vs/base/common/errors';
11
import { Emitter, Event } from '../../../util/vs/base/common/event';
12
import { ThemeIcon } from '../../../util/vs/base/common/themables';
13
import { SerializedLineEdit } from '../../../util/vs/editor/common/core/edits/lineEdit';
14
import { SerializedEdit } from './dataTypes/editUtils';
15
import { FetchCancellationError } from './dataTypes/fetchCancellationError';
16
import { LanguageContextResponse, SerializedContextResponse, serializeLanguageContext } from './dataTypes/languageContext';
17
import { RootedLineEdit } from './dataTypes/rootedLineEdit';
18
import { DebugRecorderBookmark } from './debugRecorderBookmark';
19
import { ISerializedNextEditRequest, StatelessNextEditRequest } from './statelessNextEditProvider';
20
import { stringifyChatMessages } from './utils/stringifyChatMessages';
21
import { Icon, now } from './utils/utils';
22
import { HistoryContext } from './workspaceEditTracker/historyContextProvider';
23
24
export interface MarkdownLoggable {
25
toMarkdown(): string;
26
}
27
28
/**
29
* The outcome of a log context request. Determines the icon shown in the log tree.
30
* - `pending`: no outcome yet (shows spinner or check depending on completion)
31
* - `succeeded`: model returned suggestions
32
* - `noSuggestions`: model returned no suggestions
33
* - `cached`: result is from NES cache
34
* - `cachedFromGhostText`: result is from ghost text cache
35
* - `skipped`: request was skipped or fetch-cancelled
36
* - `cancelled`: request was cancelled via CancellationToken (shown as skipped)
37
* - `errored`: an error occurred
38
* - `previouslyRejected`: result matches a suggestion that was previously rejected
39
*/
40
type LogContextOutcome = 'pending' | 'succeeded' | 'noSuggestions' | 'cached' | 'cachedFromGhostText' | 'reusedInFlight' | 'skipped' | 'cancelled' | 'errored' | 'previouslyRejected';
41
42
export class InlineEditRequestLogContext {
43
44
private static _id = 0;
45
46
public readonly requestId = InlineEditRequestLogContext._id++;
47
48
public readonly time = now();
49
50
/** Tweaks visibility of this log element in the log tree */
51
protected _isVisible: boolean = false;
52
53
get includeInLogTree(): boolean {
54
return this._isVisible;
55
}
56
57
private _isCompleted: boolean = false;
58
59
/** Mark this request as completed (no longer in progress). */
60
markCompleted(): void {
61
if (this._isCompleted) {
62
console.warn(`[InlineEditRequestLogContext] markCompleted called twice (request #${this.requestId})`);
63
}
64
this._isCompleted = true;
65
this.fireDidChange();
66
}
67
68
private readonly _onDidChange = new Emitter<void>();
69
/** Fires when state changes, allowing live log entries to refresh their content. */
70
public readonly onDidChange: Event<void> = this._onDidChange.event;
71
72
protected fireDidChange(): void {
73
this._onDidChange.fire();
74
}
75
76
constructor(
77
public readonly filePath: string,
78
public readonly version: number,
79
private _context: InlineCompletionContext | undefined,
80
) { }
81
82
public recordingBookmark: DebugRecorderBookmark | undefined = undefined;
83
84
toLogDocument(): string {
85
const lines: string[] = [];
86
lines.push('# ' + this.getMarkdownTitle() + ` (Request #${this.requestId})`);
87
88
if (!this._isCompleted) {
89
lines.push('\n⏳ **In progress…**\n');
90
}
91
92
lines.push('💡 Tip: double-click anywhere to open this file as text to copy-paste content into an issue.\n');
93
94
lines.push('<details><summary>Explanation for icons</summary>\n');
95
lines.push(`- ${Icon.lightbulbFull.svg} - model had suggestions\n`);
96
lines.push(`- ${Icon.circleSlash.svg} - model had NO suggestions\n`);
97
lines.push(`- ${Icon.database.svg} - response is from cache\n`);
98
lines.push(`- ${Icon.gitMerge.svg} - joined an in-flight request (async or speculative reuse)\n`);
99
lines.push(`- ${Icon.error.svg} - error happened\n`);
100
lines.push(`- ${Icon.skipped.svg} - fetching started but got cancelled\n`);
101
lines.push('</details>\n');
102
103
lines.push(`Inline Edit Provider: ${this._statelessNextEditProviderId ?? '<NOT-SET>'}\n`);
104
105
lines.push(`Chat Endpoint`);
106
lines.push('```');
107
lines.push(`Model name: ${this._endpointInfo?.modelName ?? '<NOT-SET>'}`);
108
lines.push(`URL: ${this._endpointInfo?.url ?? '<NOT-SET>'}`);
109
lines.push('```');
110
111
const fromCacheStatus = this._logContextOfCachedEdit ? `(cached #${this._logContextOfCachedEdit.requestId})` : '(not cached)';
112
113
lines.push(`Opportunity ID: ${this._context ? this._context.requestUuid : '<NOT-SET>'}`);
114
if (this.headerRequestId) {
115
lines.push('');
116
lines.push(`Header Request ID: ${this.headerRequestId} ${fromCacheStatus}`);
117
}
118
119
if (this._nextEditRequest) {
120
lines.push(`## Latest user edits ${fromCacheStatus}`);
121
lines.push('<details open><summary>Edit</summary>\n');
122
lines.push(this._nextEditRequest.toMarkdown());
123
lines.push('\n</details>\n');
124
}
125
126
if (this._diagnosticsResultEdit) {
127
lines.push(`## Proposed diagnostics suggestion ${this._nesTypePicked === 'diagnostics' ? '(Picked)' : '(Not Picked)'}`);
128
lines.push('<details open><summary>Edit</summary>\n');
129
lines.push('``` patch');
130
lines.push(this._diagnosticsResultEdit.toString());
131
lines.push('```');
132
lines.push('\n</details>\n');
133
}
134
135
if (this._resultEdit) {
136
lines.push(`## Proposed inline suggestion ${fromCacheStatus}`);
137
lines.push('<details open><summary>Edit</summary>\n');
138
lines.push('``` patch');
139
lines.push(this._resultEdit.toString());
140
lines.push('```');
141
lines.push('\n</details>\n');
142
}
143
144
if (this.prompt) {
145
lines.push(`## Prompt ${fromCacheStatus}`);
146
lines.push('<details><summary>Click to view</summary>\n');
147
const e = this.prompt;
148
lines.push('````');
149
lines.push(...e.split('\n'));
150
lines.push('````');
151
lines.push('\n</details>\n');
152
}
153
154
if (this.error) {
155
lines.push(`## Error ${fromCacheStatus}`);
156
lines.push('```');
157
lines.push(ErrorUtils.toString(ErrorUtils.fromUnknown(this.error)));
158
lines.push('```');
159
}
160
161
if (this.response) {
162
lines.push(`## Response ${fromCacheStatus}`);
163
lines.push('<details><summary>Click to view</summary>\n');
164
lines.push('````');
165
lines.push(this.response);
166
lines.push('````');
167
lines.push('\n</details>\n');
168
}
169
170
if (this._responseResults) {
171
lines.push(`## Response Results ${fromCacheStatus}`);
172
lines.push('<details><summary>Click to view</summary>\n');
173
lines.push('```');
174
lines.push(yaml.stringify(this._responseResults, null, '\t'));
175
lines.push('```');
176
lines.push('\n</details>\n');
177
}
178
179
if (this._isAccepted !== undefined) {
180
lines.push(`## Accepted : ${this._isAccepted ? 'Yes' : 'No'}`);
181
}
182
183
if (this._rebaseFailure) {
184
lines.push('## Rebase Failure');
185
lines.push('<details><summary>Click to view</summary>\n');
186
lines.push(this._rebaseFailure.toMarkdown());
187
lines.push('\n</details>\n');
188
}
189
190
if (this._logs.length > 0) {
191
lines.push('## Logs');
192
lines.push('<details open><summary>Logs</summary>\n');
193
lines.push(...this._logs);
194
lines.push('\n</details>\n');
195
}
196
197
lines.push(...this._renderTraceDiagram());
198
199
if (this._trace.length > 0) {
200
lines.push('## Trace');
201
lines.push('<details><summary>Trace</summary>\n');
202
lines.push('```');
203
lines.push(...this._trace);
204
lines.push('```');
205
lines.push('\n</details>\n');
206
}
207
208
return lines.join('\n');
209
}
210
211
toMinimalLog(): string {
212
// Does not include the users files, but just the relevant edits
213
const lines: string[] = [];
214
215
if (this._nesTypePicked === 'diagnostics' && this._diagnosticsResultEdit) {
216
lines.push(`## Result (Diagnostics):`);
217
lines.push('``` patch');
218
lines.push(this._diagnosticsResultEdit.toString());
219
lines.push('```');
220
} else if (this._nesTypePicked === 'llm' && this._resultEdit) {
221
lines.push(`## Result:`);
222
lines.push('``` patch');
223
if (typeof this._resultEdit === 'string') {
224
lines.push(this._resultEdit);
225
} else {
226
lines.push(this._resultEdit.toString());
227
}
228
lines.push('```');
229
} else {
230
lines.push(`## Result: <NOT-SET>`);
231
}
232
233
if (this.error) {
234
lines.push(`## Error:`);
235
lines.push('```');
236
lines.push(ErrorUtils.toString(ErrorUtils.fromUnknown(this.error)));
237
lines.push('```');
238
}
239
240
lines.push(`### Info:`);
241
lines.push(`**From cache:** ${this._logContextOfCachedEdit ? `YES (Request: ${this._logContextOfCachedEdit.requestId})` : 'NO'}`);
242
if (this._context) {
243
lines.push(`**Trigger Kind:** ${this._context.triggerKind === 0 ? 'Manual' : 'Automatic'}`);
244
lines.push(`**Request UUID:** ${this._context.requestUuid}`);
245
}
246
247
return lines.join('\n');
248
}
249
250
private _statelessNextEditProviderId: string | undefined = undefined;
251
252
setStatelessNextEditProviderId(id: string) {
253
this._statelessNextEditProviderId = id;
254
}
255
256
private _nextEditRequest: StatelessNextEditRequest | undefined = undefined;
257
258
setRequestInput(nextEditRequest: StatelessNextEditRequest): void {
259
this._isVisible = true;
260
this._nextEditRequest = nextEditRequest;
261
this.fireDidChange();
262
}
263
264
private _resultEdit: RootedLineEdit | string | undefined = undefined;
265
266
setResult(resultEditOrPatchString: RootedLineEdit | string) {
267
this._isVisible = true;
268
this._resultEdit = resultEditOrPatchString;
269
this.fireDidChange();
270
}
271
272
protected _diagnosticsResultEdit: RootedLineEdit | undefined = undefined;
273
274
setDiagnosticsResult(resultEdit: RootedLineEdit) {
275
this._isVisible = true;
276
this._diagnosticsResultEdit = resultEdit;
277
this.fireDidChange();
278
}
279
280
private _nesTypePicked: 'llm' | 'diagnostics' | undefined;
281
282
public setPickedNESType(nesTypePicked: 'llm' | 'diagnostics'): this {
283
this._nesTypePicked = nesTypePicked;
284
return this;
285
}
286
287
private _logContextOfCachedEdit: InlineEditRequestLogContext | undefined = undefined;
288
289
setIsCachedResult(logContextOfCachedEdit: InlineEditRequestLogContext): void {
290
this._logContextOfCachedEdit = logContextOfCachedEdit;
291
292
// Direct field copy — avoids triggering outcome transitions from the
293
// public setters (e.g. setResponseResults -> succeeded, setError -> errored).
294
// The final outcome is always 'cached'.
295
this.recordingBookmark = logContextOfCachedEdit.recordingBookmark;
296
this._nextEditRequest = logContextOfCachedEdit._nextEditRequest ?? this._nextEditRequest;
297
this._resultEdit = logContextOfCachedEdit._resultEdit ?? this._resultEdit;
298
this._diagnosticsResultEdit = logContextOfCachedEdit._diagnosticsResultEdit ?? this._diagnosticsResultEdit;
299
this._endpointInfo = logContextOfCachedEdit._endpointInfo ?? this._endpointInfo;
300
this._headerRequestId = logContextOfCachedEdit._headerRequestId ?? this._headerRequestId;
301
if (logContextOfCachedEdit._prompt) {
302
this._prompt = logContextOfCachedEdit._prompt;
303
}
304
this.response = logContextOfCachedEdit.response ?? this.response;
305
this._responseResults = logContextOfCachedEdit._responseResults ?? this._responseResults;
306
if (logContextOfCachedEdit.fullResponsePromise) {
307
this.setFullResponse(logContextOfCachedEdit.fullResponsePromise);
308
}
309
this._error = logContextOfCachedEdit._error ?? this._error;
310
311
this._isVisible = true;
312
this._outcome = 'cached';
313
this.fireDidChange();
314
}
315
316
/**
317
* Marks this log context as having joined an already in-flight request
318
* (async pending or speculative). The icon shows git-merge to distinguish
319
* from a true cache hit.
320
*/
321
setIsReusedInFlightResult(logContextOfReusedRequest: InlineEditRequestLogContext): void {
322
this._logContextOfCachedEdit = logContextOfReusedRequest;
323
324
this.recordingBookmark = logContextOfReusedRequest.recordingBookmark;
325
this._nextEditRequest = logContextOfReusedRequest._nextEditRequest ?? this._nextEditRequest;
326
this._resultEdit = logContextOfReusedRequest._resultEdit ?? this._resultEdit;
327
this._diagnosticsResultEdit = logContextOfReusedRequest._diagnosticsResultEdit ?? this._diagnosticsResultEdit;
328
this._endpointInfo = logContextOfReusedRequest._endpointInfo ?? this._endpointInfo;
329
this._headerRequestId = logContextOfReusedRequest._headerRequestId ?? this._headerRequestId;
330
if (logContextOfReusedRequest._prompt) {
331
this._prompt = logContextOfReusedRequest._prompt;
332
}
333
this.response = logContextOfReusedRequest.response ?? this.response;
334
this._responseResults = logContextOfReusedRequest._responseResults ?? this._responseResults;
335
if (logContextOfReusedRequest.fullResponsePromise) {
336
this.setFullResponse(logContextOfReusedRequest.fullResponsePromise);
337
}
338
this._error = logContextOfReusedRequest._error ?? this._error;
339
340
this._isVisible = true;
341
this._outcome = 'reusedInFlight';
342
this.fireDidChange();
343
}
344
345
private _endpointInfo: { url: string; modelName: string } | undefined;
346
347
public setEndpointInfo(url: string, modelName: string): void {
348
this._endpointInfo = { url, modelName };
349
this.fireDidChange();
350
}
351
352
public get endpointInfo(): { url: string; modelName: string } | undefined {
353
return this._endpointInfo;
354
}
355
356
private _headerRequestId: string | undefined = undefined;
357
public setHeaderRequestId(headerRequestId: string): void {
358
this._headerRequestId = headerRequestId;
359
this.fireDidChange();
360
}
361
get headerRequestId(): string | undefined {
362
return this._headerRequestId;
363
}
364
365
public _prompt: string | undefined = undefined;
366
private _rawMessages: Raw.ChatMessage[] | undefined = undefined;
367
368
get prompt(): string | undefined {
369
return this._prompt;
370
}
371
372
get rawMessages(): Raw.ChatMessage[] | undefined {
373
return this._rawMessages;
374
}
375
376
setPrompt(prompt: string | Raw.ChatMessage[]) {
377
this._isVisible = true;
378
if (typeof prompt === 'string') {
379
this._prompt = prompt;
380
} else {
381
this._rawMessages = prompt;
382
this._prompt = stringifyChatMessages(prompt);
383
}
384
this.fireDidChange();
385
}
386
387
private _outcome: LogContextOutcome = 'pending';
388
389
/**
390
* Sets the outcome, warning if already set (i.e., not `pending`).
391
* Use direct `this._outcome = ...` assignment to bypass the guard
392
* (e.g., in `setIsCachedResult` which intentionally overrides any inherited outcome).
393
*/
394
private _setOutcome(outcome: LogContextOutcome): void {
395
// 'reusedInFlight' is an intermediate state set when joining an in-flight
396
// request (before the result arrives), so it can legitimately transition
397
// to the final outcome (skipped, errored, etc.) just like 'pending'.
398
if (this._outcome !== 'pending' && this._outcome !== 'reusedInFlight') {
399
console.warn(`[InlineEditRequestLogContext] outcome transition from '${this._outcome}' to '${outcome}' (request #${this.requestId})`);
400
}
401
this._outcome = outcome;
402
}
403
404
private _resolveIcon(): Icon.t {
405
switch (this._outcome) {
406
case 'pending': return this._isCompleted ? Icon.check : Icon.loading;
407
case 'succeeded': return Icon.lightbulbFull;
408
case 'noSuggestions': return Icon.circleSlash;
409
case 'cached':
410
case 'cachedFromGhostText': return Icon.database;
411
case 'reusedInFlight': return Icon.gitMerge;
412
case 'skipped':
413
case 'cancelled': return Icon.skipped;
414
case 'errored': return Icon.error;
415
case 'previouslyRejected': return Icon.thumbsdown;
416
}
417
}
418
419
getIcon(): ThemeIcon {
420
return this._resolveIcon().themeIcon;
421
}
422
423
public setIsSkipped() {
424
this._setOutcome('skipped');
425
this._isVisible = false;
426
this.fireDidChange();
427
}
428
429
public markAsFromCache() {
430
this._setOutcome('cachedFromGhostText');
431
this._isVisible = true;
432
this.fireDidChange();
433
}
434
435
public markAsNoSuggestions() {
436
this._setOutcome('noSuggestions');
437
this._isVisible = true;
438
this.fireDidChange();
439
}
440
441
public markAsPreviouslyRejected() {
442
// Direct assignment — bypasses _setOutcome guard because this transition
443
// legitimately overrides 'succeeded' when a fetched edit turns out to be rejected.
444
this._outcome = 'previouslyRejected';
445
this._isVisible = true;
446
this.fireDidChange();
447
}
448
449
private _error: unknown | undefined = undefined;
450
451
get error(): unknown | undefined {
452
return this._error;
453
}
454
455
setError(e: unknown): void {
456
this._isVisible = true;
457
this._error = e;
458
459
if (this._error instanceof FetchCancellationError) {
460
this._setOutcome('skipped');
461
} else if (isCancellationError(this._error)) {
462
this._setOutcome('cancelled');
463
this._isVisible = false;
464
} else {
465
this._setOutcome('errored');
466
}
467
this.fireDidChange();
468
}
469
470
/**
471
* Model Response
472
*/
473
private response: string | undefined = undefined;
474
setResponse(v: string): void {
475
this._isVisible = true;
476
this.response = v;
477
this.fireDidChange();
478
}
479
480
private fullResponsePromise: Promise<string | undefined> | undefined = undefined;
481
private fullResponse: string | undefined = undefined;
482
setFullResponse(promise: Promise<string | undefined>): void {
483
this.fullResponsePromise = promise;
484
promise.then(response => this.fullResponse = response);
485
}
486
487
async allPromisesResolved(): Promise<void> {
488
await this.fullResponsePromise;
489
}
490
491
private providerStartTime: number | undefined = undefined;
492
setProviderStartTime(): void {
493
this.providerStartTime = Date.now();
494
this.fireDidChange();
495
}
496
497
private providerEndTime: number | undefined = undefined;
498
setProviderEndTime(): void {
499
this.providerEndTime = Date.now();
500
this.fireDidChange();
501
}
502
503
private fetchStartTime: number | undefined = undefined;
504
setFetchStartTime(): void {
505
this.fetchStartTime = Date.now();
506
this.fireDidChange();
507
}
508
509
private fetchEndTime: number | undefined = undefined;
510
setFetchEndTime(): void {
511
this.fetchEndTime = Date.now();
512
this.fireDidChange();
513
}
514
515
/**
516
* Each of edit suggestions from model
517
*/
518
private _responseResults: readonly unknown[] | undefined = undefined;
519
520
get responseResults(): readonly unknown[] | undefined {
521
return this._responseResults;
522
}
523
524
setResponseResults(v: readonly unknown[]): void {
525
this._isVisible = true;
526
this._responseResults = v;
527
if (this._outcome === 'pending') {
528
this._outcome = 'succeeded';
529
}
530
this.fireDidChange();
531
}
532
533
getDebugName(): string {
534
return `NES | ${basename(this.filePath)} (v${this.version})`;
535
}
536
537
getMarkdownTitle(): string {
538
const icon = this._resolveIcon();
539
return `${icon.svg} ` + this.getDebugName();
540
}
541
542
protected _recentEdit: HistoryContext | undefined = undefined;
543
544
setRecentEdit(edit: HistoryContext): void {
545
this._recentEdit = edit;
546
}
547
548
private _trace: string[] = [];
549
trace(msg: string): void {
550
this._trace.push(msg);
551
this.fireDidChange();
552
}
553
554
private _renderTraceDiagram(): string[] {
555
if (this._trace.length === 0) {
556
return [];
557
}
558
559
const lines: string[] = [];
560
lines.push('## Trace Diagram');
561
lines.push('<details open><summary>Trace Diagram</summary>\n');
562
lines.push('```');
563
564
// Parse trace lines into structured data
565
const parsedTraces = this._trace.map(line => {
566
const timeMatch = line.match(/^\[\s*(\d+)ms\]/);
567
const timestamp = timeMatch ? parseInt(timeMatch[1], 10) : 0;
568
569
// Extract the bracketed path segments and the message
570
const afterTime = line.replace(/^\[\s*\d+ms\]\s*/, '');
571
const segments: string[] = [];
572
let remaining = afterTime;
573
let bracketMatch;
574
while ((bracketMatch = remaining.match(/^\[([^\]]+)\]/))) {
575
segments.push(bracketMatch[1]);
576
remaining = remaining.slice(bracketMatch[0].length);
577
}
578
const message = remaining.trim();
579
580
return { timestamp, segments, message };
581
});
582
583
if (parsedTraces.length === 0) {
584
lines.push('(no trace data)');
585
lines.push('```');
586
lines.push('\n</details>\n');
587
return lines;
588
}
589
590
// Find the maximum timestamp for time width calculation
591
const maxTime = Math.max(...parsedTraces.map(t => t.timestamp));
592
const timeWidth = Math.max(6, String(maxTime).length + 3);
593
594
// Build a map of segment paths to track when they start/end
595
const activeSegments = new Map<string, { startTime: number; depth: number }>();
596
const segmentLifetimes: { path: string; startTime: number; endTime: number; depth: number; name: string }[] = [];
597
598
parsedTraces.forEach((trace, idx) => {
599
const currentPath = trace.segments.join('|');
600
601
// Check for segments that are no longer active
602
for (const [path, info] of activeSegments) {
603
if (!currentPath.startsWith(path) && currentPath !== path) {
604
segmentLifetimes.push({
605
path,
606
startTime: info.startTime,
607
endTime: trace.timestamp,
608
depth: info.depth,
609
name: path.split('|').pop() || ''
610
});
611
activeSegments.delete(path);
612
}
613
}
614
615
// Add new segments
616
let pathSoFar = '';
617
trace.segments.forEach((segment, depth) => {
618
pathSoFar = pathSoFar ? `${pathSoFar}|${segment}` : segment;
619
if (!activeSegments.has(pathSoFar)) {
620
activeSegments.set(pathSoFar, { startTime: trace.timestamp, depth });
621
}
622
});
623
});
624
625
// Close any remaining active segments
626
const lastTimestamp = parsedTraces[parsedTraces.length - 1]?.timestamp || 0;
627
for (const [path, info] of activeSegments) {
628
segmentLifetimes.push({
629
path,
630
startTime: info.startTime,
631
endTime: lastTimestamp,
632
depth: info.depth,
633
name: path.split('|').pop() || ''
634
});
635
}
636
637
// Render timeline header
638
lines.push('');
639
lines.push('Timeline (nested call hierarchy):');
640
lines.push('─'.repeat(60));
641
642
// Track what's currently shown at each depth to avoid redundant output
643
const currentAtDepth: string[] = [];
644
645
for (const trace of parsedTraces) {
646
const timeStr = `[${String(trace.timestamp).padStart(timeWidth - 3)}ms]`;
647
const indentUnit = '│ ';
648
const newBranchUnit = '├── ';
649
650
// Determine which segments are new vs continuing
651
let indent = '';
652
let displaySegment = '';
653
let hasNewSegment = false;
654
655
for (let d = 0; d < trace.segments.length; d++) {
656
const seg = trace.segments[d];
657
if (currentAtDepth[d] !== seg) {
658
// This is a new segment at this depth
659
hasNewSegment = true;
660
currentAtDepth[d] = seg;
661
// Clear deeper levels
662
currentAtDepth.length = d + 1;
663
displaySegment = seg;
664
indent = indentUnit.repeat(d);
665
break;
666
}
667
indent = indentUnit.repeat(d + 1);
668
}
669
670
if (hasNewSegment) {
671
// Show the new segment
672
const prefix = indent + newBranchUnit;
673
lines.push(`${timeStr} ${prefix}[${displaySegment}]`);
674
if (trace.message) {
675
const msgIndent = indentUnit.repeat(trace.segments.length);
676
lines.push(`${' '.repeat(timeWidth + 1)} ${msgIndent}↳ ${trace.message}`);
677
}
678
} else if (trace.message) {
679
// Just a message at the current depth
680
const msgIndent = indentUnit.repeat(trace.segments.length);
681
lines.push(`${timeStr} ${msgIndent}↳ ${trace.message}`);
682
}
683
}
684
685
lines.push('─'.repeat(60));
686
lines.push('```');
687
lines.push('\n</details>\n');
688
689
return lines;
690
}
691
692
private _logs: string[] = [];
693
addLog(content: string): void {
694
this._logs.push(content.replace('\n', '\\n').replace('\t', '\\t').replace('`', '\`') + '\n');
695
this.fireDidChange();
696
}
697
698
private _rebaseFailure: MarkdownLoggable | undefined;
699
700
setRebaseFailure(failure: MarkdownLoggable): void {
701
this._rebaseFailure = failure;
702
}
703
704
705
private _isAccepted: boolean | undefined = undefined;
706
setAccepted(isAccepted: boolean): void {
707
this._isAccepted = isAccepted;
708
}
709
710
addListToLog(list: string[]): void {
711
list.forEach(l => this.addLog(`- ${l}`));
712
}
713
714
addCodeblockToLog(code: string, language: string = ''): void {
715
this._logs.push(`\`\`\`${language}\n${code}\n\`\`\`\n`);
716
}
717
718
private _fileDiagnostics: string | undefined;
719
setDiagnosticsData(fileDiagnostics: string): void {
720
this._fileDiagnostics = fileDiagnostics;
721
}
722
723
private _terminalOutput: string | undefined;
724
setTerminalData(terminalOutput: string): void {
725
this._terminalOutput = terminalOutput;
726
}
727
728
private _languageContext: LanguageContextResponse | undefined;
729
setLanguageContext(langCtx: LanguageContextResponse): void {
730
this._languageContext = langCtx;
731
}
732
733
/**
734
* Convert the current instance into a JSON format to enable serialization
735
* @returns JSON representation of the current state
736
*/
737
toJSON(): ISerializedInlineEditLogContext {
738
return {
739
requestId: this.requestId,
740
time: this.time,
741
filePath: this.filePath,
742
version: this.version,
743
statelessNextEditProviderId: this._statelessNextEditProviderId,
744
nextEditRequest: this._nextEditRequest?.serialize(),
745
diagnosticsResultEdit: this._diagnosticsResultEdit?.toString(),
746
resultEdit: this._resultEdit?.toString(),
747
isCachedResult: !!this._logContextOfCachedEdit,
748
prompt: this.prompt,
749
error: String(this.error),
750
response: this.fullResponse,
751
responseResults: yaml.stringify(this._responseResults, null, '\t'),
752
providerStartTime: this.providerStartTime,
753
providerEndTime: this.providerEndTime,
754
fetchStartTime: this.fetchStartTime,
755
fetchEndTime: this.fetchEndTime,
756
logs: this._logs,
757
isAccepted: this._isAccepted,
758
languageContext: this._languageContext ? serializeLanguageContext(this._languageContext) : undefined,
759
diagnostics: this._fileDiagnostics,
760
terminalOutput: this._terminalOutput,
761
};
762
}
763
}
764
765
function basename(path: string): string {
766
const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
767
if (slash === -1) { return path; }
768
return path.slice(slash + 1);
769
}
770
771
export interface INextEditProviderTest {
772
// from least recent to most recent
773
recentWorkspaceEdits: { path: string; initialText: string; edit: SerializedEdit }[];
774
recentWorkspaceEditsActiveDocumentIdx?: number; // by default the last document
775
statelessDocuments?: { initialText: string; edit: SerializedLineEdit }[];
776
statelessActiveDocumentIdx?: number; // by default the last document
777
statelessLLMPrompt?: string;
778
statelessLLMResponse?: string;
779
statelessNextEdit?: SerializedLineEdit;
780
781
nextEdit?: SerializedEdit;
782
}
783
784
export interface ISerializedInlineEditLogContext {
785
requestId: number;
786
time: number;
787
filePath: string;
788
version: number;
789
statelessNextEditProviderId: string | undefined;
790
nextEditRequest: ISerializedNextEditRequest | undefined;
791
diagnosticsResultEdit: string | undefined;
792
resultEdit: string | undefined;
793
isCachedResult: boolean;
794
prompt: string | undefined;
795
error: string;
796
response: string | undefined;
797
responseResults: string;
798
providerStartTime: number | undefined;
799
providerEndTime: number | undefined;
800
fetchStartTime: number | undefined;
801
fetchEndTime: number | undefined;
802
logs: string[];
803
isAccepted: boolean | undefined;
804
languageContext: SerializedContextResponse | undefined;
805
diagnostics: string | undefined;
806
terminalOutput: string | undefined;
807
}
808
809