-
Notifications
You must be signed in to change notification settings - Fork 39.3k
Expand file tree
/
Copy pathcopilotcliSessionService.ts
More file actions
1388 lines (1250 loc) · 61.8 KB
/
copilotcliSessionService.ts
File metadata and controls
1388 lines (1250 loc) · 61.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { internal, LocalSessionMetadata, Session, SessionContext, SessionEvent, SessionOptions, SweCustomAgent } from '@github/copilot/sdk';
import * as l10n from '@vscode/l10n';
import { createReadStream } from 'node:fs';
import * as fs from 'node:fs/promises';
import { devNull, EOL } from 'node:os';
import { createInterface } from 'node:readline';
import type { ChatCustomAgent, ChatRequest, ChatSessionItem } from 'vscode';
import { IChatDebugFileLoggerService } from '../../../../platform/chat/common/chatDebugFileLoggerService';
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
import { INativeEnvService } from '../../../../platform/env/common/envService';
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
import { RelativePattern } from '../../../../platform/filesystem/common/fileTypes';
import { ILogService } from '../../../../platform/log/common/logService';
import { deriveCopilotCliOTelEnv } from '../../../../platform/otel/common/agentOTelEnv';
import { IOTelService } from '../../../../platform/otel/common/otelService';
import { IPromptsService } from '../../../../platform/promptFiles/common/promptsService';
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
import { createServiceIdentifier } from '../../../../util/common/services';
import { coalesce } from '../../../../util/vs/base/common/arrays';
import { disposableTimeout, raceCancellation, raceCancellationError, SequencerByKey, ThrottledDelayer } from '../../../../util/vs/base/common/async';
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
import { CancellationError } from '../../../../util/vs/base/common/errors';
import { Emitter, Event } from '../../../../util/vs/base/common/event';
import { Lazy } from '../../../../util/vs/base/common/lazy';
import { Disposable, DisposableMap, IDisposable, IReference, RefCountedDisposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
import { basename, dirname, isEqual, joinPath } from '../../../../util/vs/base/common/resources';
import { URI } from '../../../../util/vs/base/common/uri';
import { generateUuid } from '../../../../util/vs/base/common/uuid';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { ChatRequestTurn2, ChatResponseTurn2, ChatSessionStatus, Uri } from '../../../../vscodeTypes';
import { IPromptVariablesService } from '../../../prompt/node/promptVariablesService';
import { IAgentSessionsWorkspace } from '../../common/agentSessionsWorkspace';
import { IChatSessionMetadataStore, RequestDetails, StoredModeInstructions } from '../../common/chatSessionMetadataStore';
import { IChatSessionWorkspaceFolderService } from '../../common/chatSessionWorkspaceFolderService';
import { IChatSessionWorktreeService } from '../../common/chatSessionWorktreeService';
import { isUntitledSessionId } from '../../common/utils';
import { emptyWorkspaceInfo, getWorkingDirectory, IWorkspaceInfo } from '../../common/workspaceInfo';
import { buildChatHistoryFromEvents, RequestIdDetails, stripReminders } from '../common/copilotCLITools';
import { ICustomSessionTitleService } from '../common/customSessionTitleService';
import { IChatDelegationSummaryService } from '../common/delegationSummaryService';
import { SessionIdForCLI } from '../common/utils';
import { getCopilotCLISessionDir, getCopilotCLISessionEventsFile, getCopilotCLIWorkspaceFile } from './cliHelpers';
import { getAgentFileNameFromFilePath, ICopilotCLIAgents, ICopilotCLISDK } from './copilotCli';
import { CopilotCliBridgeSpanProcessor } from './copilotCliBridgeSpanProcessor';
import { CopilotCLISession, ICopilotCLISession } from './copilotcliSession';
import { ICopilotCLISkills } from './copilotCLISkills';
import { ICopilotCLIMCPHandler, McpServerMappings, remapCustomAgentTools } from './mcpHandler';
const COPILOT_CLI_WORKSPACE_JSON_FILE_KEY = 'github.copilot.cli.workspaceSessionFile';
export interface ICopilotCLISessionItem {
readonly id: string;
readonly label: string;
readonly timing: ChatSessionItem['timing'];
readonly status?: ChatSessionStatus;
readonly workingDirectory?: Uri;
}
export type ExtendedChatRequest = ChatRequest & { prompt: string };
export type ISessionOptions = {
model?: string;
reasoningEffort?: string;
workspace: IWorkspaceInfo;
agent?: SweCustomAgent;
debugTargetSessionIds?: readonly string[];
mcpServerMappings?: McpServerMappings;
additionalWorkspaces?: IWorkspaceInfo[];
}
export type IGetSessionOptions = ISessionOptions & { sessionId: string };
export type ICreateSessionOptions = ISessionOptions & { sessionId?: string };
export interface ICopilotCLISessionService {
readonly _serviceBrand: undefined;
/**
* @deprecated Kept only for non-controller API
*/
onDidChangeSessions: Event<void>;
onDidDeleteSession: Event<string>;
onDidChangeSession: Event<ICopilotCLISessionItem>;
onDidCreateSession: Event<ICopilotCLISessionItem>;
getSessionWorkingDirectory(sessionId: string): Uri | undefined;
// Session metadata querying
getSessionItem(sessionId: string, token: CancellationToken): Promise<ICopilotCLISessionItem | undefined>;
getAllSessions(token: CancellationToken): Promise<readonly ICopilotCLISessionItem[]>;
// SDK session management
createNewSessionId(): string;
isNewSessionId(sessionId: string): boolean;
deleteSession(sessionId: string): Promise<void>;
// Session rename
renameSession(sessionId: string, title: string): Promise<void>;
// Session wrapper tracking
getSession(options: IGetSessionOptions, token: CancellationToken): Promise<IReference<ICopilotCLISession> | undefined>;
createSession(options: ICreateSessionOptions, token: CancellationToken): Promise<IReference<ICopilotCLISession>>;
getChatHistory(options: { sessionId: string; workspace: IWorkspaceInfo }, token: CancellationToken): Promise<(ChatRequestTurn2 | ChatResponseTurn2)[]>;
/**
* @deprecated Use `forkSession` instead
*/
forkSessionV1(options: { sessionId: string; requestId: string | undefined; workspace: IWorkspaceInfo }, token: CancellationToken): Promise<string>;
forkSession(options: { sessionId: string; requestId: string | undefined; workspace: IWorkspaceInfo }, token: CancellationToken): Promise<string>;
tryGetPartialSesionHistory(sessionId: string): Promise<readonly (ChatRequestTurn2 | ChatResponseTurn2)[] | undefined>;
}
export const ICopilotCLISessionService = createServiceIdentifier<ICopilotCLISessionService>('ICopilotCLISessionService');
const SESSION_SHUTDOWN_TIMEOUT_MS = 300 * 1000;
export class CopilotCLISessionService extends Disposable implements ICopilotCLISessionService {
declare _serviceBrand: undefined;
private _sessionManager: Lazy<Promise<internal.LocalSessionManager>>;
private _sessionWrappers = new DisposableMap<string, RefCountedSession>();
private readonly _partialSessionHistories = new Map<string, readonly (ChatRequestTurn2 | ChatResponseTurn2)[]>();
private readonly _onDidChangeSessions = this._register(new Emitter<void>());
public readonly onDidChangeSessions = this._onDidChangeSessions.event;
private readonly _onDidDeleteSession = this._register(new Emitter<string>());
public readonly onDidDeleteSession = this._onDidDeleteSession.event;
private readonly _onDidChangeSession = this._register(new Emitter<ICopilotCLISessionItem>());
public readonly onDidChangeSession = this._onDidChangeSession.event;
private readonly _onDidCreateSession = this._register(new Emitter<ICopilotCLISessionItem>());
public readonly onDidCreateSession = this._onDidCreateSession.event;
private readonly _onDidCloseSession = this._register(new Emitter<string>());
private readonly sessionTerminators = new DisposableMap<string, IDisposable>();
private sessionMutexForGetSession = new Map<string, Mutex>();
private readonly _sessionTracker: CopilotCLISessionWorkspaceTracker;
private readonly _sessionWorkingDirectories = new Map<string, Uri | undefined>();
private readonly _onDidChangeSessionsThrottler = this._register(new ThrottledDelayer<void>(500));
private readonly _cachedSessionItems = new Map<string, ICopilotCLISessionItem>();
private readonly _sessionsBeingCreatedViaFork = new Set<string>();
private readonly _newSessionIds = new Set<string>();
/** Bridge processor that forwards SDK native OTel spans to the debug panel. */
private _bridgeProcessor: CopilotCliBridgeSpanProcessor | undefined;
/** Whether we've attempted to install the bridge (only try once). */
private _bridgeInstalled = false;
constructor(
@ILogService protected readonly logService: ILogService,
@ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@INativeEnvService private readonly nativeEnv: INativeEnvService,
@IFileSystemService private readonly fileSystem: IFileSystemService,
@ICopilotCLIMCPHandler private readonly mcpHandler: ICopilotCLIMCPHandler,
@ICopilotCLIAgents private readonly agents: ICopilotCLIAgents,
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
@ICustomSessionTitleService private readonly customSessionTitleService: ICustomSessionTitleService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ICopilotCLISkills private readonly copilotCLISkills: ICopilotCLISkills,
@IChatDelegationSummaryService private readonly _delegationSummaryService: IChatDelegationSummaryService,
@IChatSessionMetadataStore private readonly _chatSessionMetadataStore: IChatSessionMetadataStore,
@IAgentSessionsWorkspace private readonly _agentSessionsWorkspace: IAgentSessionsWorkspace,
@IChatSessionWorkspaceFolderService private readonly workspaceFolderService: IChatSessionWorkspaceFolderService,
@IChatSessionWorktreeService private readonly worktreeManager: IChatSessionWorktreeService,
@IOTelService private readonly _otelService: IOTelService,
@IPromptVariablesService private readonly _promptVariablesService: IPromptVariablesService,
@IChatDebugFileLoggerService private readonly _debugFileLogger: IChatDebugFileLoggerService,
@IPromptsService private readonly _promptsService: IPromptsService,
) {
super();
this.monitorSessionFiles();
this._sessionManager = new Lazy<Promise<internal.LocalSessionManager>>(async () => {
try {
const { internal, createLocalFeatureFlagService, noopTelemetryBinder } = await this.getSDKPackage();
// Always enable SDK OTel so the debug panel receives native spans via the bridge.
// When user OTel is disabled, we force file exporter to /dev/null so the SDK
// creates OtelSessionTracker (for debug panel) but doesn't export to any collector.
if (!process.env['COPILOT_OTEL_ENABLED']) {
process.env['COPILOT_OTEL_ENABLED'] = 'true';
}
// Default content capture to 'true' for the debug panel. When user OTel
// is enabled, their captureContent setting overrides this default below.
// When user OTel is disabled, the default gives debug panel content.
// If the user explicitly set the env var, respect their choice.
if (!process.env['OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT']) {
process.env['OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT'] = 'true';
}
if (this._otelService.config.enabled) {
const otelEnv = deriveCopilotCliOTelEnv(this._otelService.config);
for (const [key, value] of Object.entries(otelEnv)) {
process.env[key] = value;
}
// When user OTel is enabled, their captureContent config takes
// precedence over the debug-panel default set above.
process.env['OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT'] = String(this._otelService.config.captureContent);
} else {
// User OTel disabled: ensure SDK doesn't export to any external collector.
// Use file exporter to /dev/null so the SDK creates OtelSessionTracker
// (for debug panel) but writes spans nowhere.
process.env['COPILOT_OTEL_EXPORTER_TYPE'] = 'file';
process.env['COPILOT_OTEL_FILE_EXPORTER_PATH'] = devNull;
}
return new internal.LocalSessionManager({
featureFlagService: createLocalFeatureFlagService(),
telemetryService: new internal.NoopTelemetryService(),
telemetryBinder: noopTelemetryBinder
}, { flushDebounceMs: undefined, settings: undefined, version: undefined });
}
catch (error) {
this.logService.error(`Failed to initialize Copilot CLI Session Manager: ${error}`);
throw error;
}
});
this._sessionTracker = this.instantiationService.createInstance(CopilotCLISessionWorkspaceTracker);
}
private async getSDKPackage() {
const { internal, LocalSession, createLocalFeatureFlagService, noopTelemetryBinder } = await this.copilotCLISDK.getPackage();
return { internal, LocalSession, createLocalFeatureFlagService, noopTelemetryBinder };
}
getSessionWorkingDirectory(sessionId: string): Uri | undefined {
return this._sessionWorkingDirectories.get(sessionId);
}
private triggerSessionsChangeEvent() {
// If we're busy fetching sessions, then do not trigger change event as we'll trigger one after we're done fetching sessions.
if (this._isGettingSessions > 0) {
return;
}
this._onDidChangeSessionsThrottler.trigger(() => Promise.resolve(this._onDidChangeSessions.fire()));
}
public createNewSessionId(): string {
const sessionId = generateUuid();
this._newSessionIds.add(sessionId);
return sessionId;
}
public isNewSessionId(sessionId: string): boolean {
return this._newSessionIds.has(sessionId);
}
protected monitorSessionFiles() {
try {
const sessionDir = joinPath(this.nativeEnv.userHome, '.copilot', 'session-state');
const watcher = this._register(this.fileSystem.createFileSystemWatcher(new RelativePattern(sessionDir, '**/*.jsonl')));
this._register(watcher.onDidCreate(async (e) => {
const sessionId = extractSessionIdFromEventPath(sessionDir, e);
if (sessionId && this._sessionsBeingCreatedViaFork.has(sessionId)) {
return;
}
this.triggerSessionsChangeEvent();
const sessionItem = sessionId ? await this.getSessionItemImpl(sessionId, 'disk', CancellationToken.None) : undefined;
if (sessionItem) {
this._onDidChangeSession.fire(sessionItem);
}
}));
this._register(watcher.onDidDelete(e => {
const sessionId = extractSessionIdFromEventPath(sessionDir, e);
if (sessionId) {
this._cachedSessionItems.delete(sessionId);
this._onDidDeleteSession.fire(sessionId);
}
this.triggerSessionsChangeEvent();
}));
this._register(watcher.onDidChange((e) => {
// If we're busy fetching sessions, then do not trigger change event as we'll trigger one after we're done fetching sessions.
if (this._isGettingSessions > 0) {
return;
}
const sessionId = extractSessionIdFromEventPath(sessionDir, e);
if (sessionId && this._sessionsBeingCreatedViaFork.has(sessionId)) {
return;
}
// If we're already working on a session that we're aware of then no need to trigger a refresh.
if (Array.from(this._sessionWrappers.keys()).some(sessionId => e.path.includes(sessionId))) {
return;
}
if (sessionId) {
this.triggerOnDidChangeSessionItem(sessionId, 'fileSystemChange');
}
this.triggerSessionsChangeEvent();
}));
} catch (error) {
this.logService.error(`Failed to monitor Copilot CLI session files: ${error}`);
}
}
async getSessionManager() {
return this._sessionManager.value;
}
private _sessionChangeNotifierByKey = new SequencerByKey<string>();
private triggerOnDidChangeSessionItem(sessionId: string, reason: 'fileSystemChange' | 'statusChange') {
this._sessionChangeNotifierByKey.queue(sessionId, async () => {
// lets wait for 500ms, as we could get a lot of change events in a short period of time.
// E.g. if you have a session running in integrated terminal, then its possible we will see a lot of updates.
// In such cases its best to just delay (throttle) by 500ms (we get that via the sequncer and this delay)
if (reason === 'fileSystemChange') {
await new Promise<void>(resolve => disposableTimeout(resolve, 500, this._store));
// If already getting all sessions, no point in triggering individual change event.
if (this._isGettingSessions > 0) {
return;
}
}
const sessionItem = await this.getSessionItemImpl(sessionId, reason === 'statusChange' ? 'inMemorySession' : 'disk', CancellationToken.None);
if (sessionItem) {
this._onDidChangeSession.fire(sessionItem);
}
}).catch(error => {
this.logService.error(`Failed to trigger session change event for session ${sessionId}: ${error}`);
});
}
/**
* This can be very expensive, as this involves loading all of the sessions.
* TODO @DonJayamanne We need to try to use SDK to open a session and get the details.
*/
public async getSessionItem(sessionId: string, token: CancellationToken): Promise<ICopilotCLISessionItem | undefined> {
return this.getSessionItemImpl(sessionId, 'inMemorySession', token);
}
public async getSessionItemImpl(sessionId: string, source: 'inMemorySession' | 'disk', token: CancellationToken): Promise<ICopilotCLISessionItem | undefined> {
const wrappedSession = this._sessionWrappers.get(sessionId);
// Give preference to the session we have in memory, as this contains the latest information.
if (wrappedSession && (source === 'inMemorySession' || wrappedSession.object.status === ChatSessionStatus.InProgress)) {
const item = await this.constructSessionItemFromWrappedSession(wrappedSession, token);
if (item) {
return item;
}
}
// // We can get the item from cache, as the ICopilotCLISessionItem doesn't store anything that changes.
// // Except the title
// let item = this._cachedSessionItems.get(sessionId);
// if (item) {
// // Since this was a change event for an existing session, we must get the latest title.
// const label = await this.getSessionTitle(sessionId, CancellationToken.None);
// const sessionItem = Object.assign({}, item, { label });
// return sessionItem;
// }
const sessionManager = await raceCancellation(this.getSessionManager(), token);
const metadata = sessionManager ? await raceCancellationError(sessionManager.getSessionMetadata({ sessionId }), token) : undefined;
if (!metadata || token.isCancellationRequested) {
return;
}
await this._sessionTracker.initialize();
return await this.constructSessionItem(metadata, token);
}
public async getSessionTitle(sessionId: string, token: CancellationToken): Promise<string> {
return this.getSessionTitleImpl(sessionId, undefined, token);
}
/**
* Gets the session title.
* Always give preference to label defined by user, then title from CLI session object.
* If we have the metadata then use that over extracting label ourselves or using any cache.
*/
private async getSessionTitleImpl(sessionId: string, metadata: LocalSessionMetadata | undefined, token: CancellationToken): Promise<string> {
// Always give preference to label defined by user, then title from CLI and finally label from prompt summary. This is to ensure that if user has renamed the session, we do not override that with title from CLI or label from prompt.
const accurateTitle = await this.customSessionTitleService.getCustomSessionTitle(sessionId) ??
labelFromPrompt(this._sessionWrappers.get(sessionId)?.object.pendingPrompt ?? '') ??
this._sessionWrappers.get(sessionId)?.object.title;
if (accurateTitle) {
return accurateTitle;
}
const summarizedTitle = labelFromPrompt(metadata?.summary ?? '');
if (summarizedTitle) {
if (summarizedTitle.endsWith('...')) {
// If the SDK is going to just give us a truncated version of the first user message as the summary, then we might as well extract the label ourselves from the first user message instead of using the truncated summary.
} else {
return summarizedTitle;
}
}
const firstUserMessage = await this.getFirstUserMessageFromSession(sessionId, token);
return labelFromPrompt(firstUserMessage ?? '');
}
private _getAllSessionsProgress: Promise<readonly ICopilotCLISessionItem[]> | undefined;
private _isGettingSessions: number = 0;
async getAllSessions(token: CancellationToken): Promise<readonly ICopilotCLISessionItem[]> {
if (!this._getAllSessionsProgress) {
this._getAllSessionsProgress = this._getAllSessions(token);
}
return this._getAllSessionsProgress.finally(() => {
this._getAllSessionsProgress = undefined;
});
}
private _sessionLabels: Map<string, string> = new Map();
async _getAllSessions(token: CancellationToken): Promise<readonly ICopilotCLISessionItem[]> {
this._isGettingSessions++;
try {
const sessionManager = await raceCancellationError(this.getSessionManager(), token);
const sessionMetadataList = await raceCancellationError(sessionManager.listSessions(), token);
await this._sessionTracker.initialize();
// Convert SessionMetadata to ICopilotCLISession
const diskSessions: ICopilotCLISessionItem[] = coalesce(await Promise.all(
sessionMetadataList.map(async (metadata): Promise<ICopilotCLISessionItem | undefined> => {
const workingDirectory = metadata.context?.cwd ? URI.file(metadata.context.cwd) : undefined;
this._sessionWorkingDirectories.set(metadata.sessionId, workingDirectory);
if (!await this.shouldShowSession(metadata.sessionId, metadata.context)) {
return;
}
const id = metadata.sessionId;
const startTime = metadata.startTime.getTime();
const endTime = metadata.modifiedTime.getTime();
const label = await this.customSessionTitleService.getCustomSessionTitle(metadata.sessionId) ?? this._sessionWrappers.get(metadata.sessionId)?.object.title ?? this._sessionLabels.get(metadata.sessionId) ?? (metadata.summary ? labelFromPrompt(metadata.summary) : undefined);
// CLI adds `<current_datetime>` tags to user prompt, this needs to be removed.
// However in summary CLI can end up truncating the prompt and adding `... <current_dateti...` at the end.
// So if we see a `<` in the label, we need to load the session to get the first user message.
if (label && !label.includes('<')) {
return {
id,
label,
timing: { created: startTime, startTime, endTime },
workingDirectory
};
}
try {
const firstUserMessage = await this.getFirstUserMessageFromSession(metadata.sessionId, token);
const label = labelFromPrompt(firstUserMessage ?? metadata.summary ?? '');
if (!label) {
return;
}
this._sessionLabels.set(metadata.sessionId, label);
return {
id,
label,
timing: { created: startTime, startTime, endTime },
workingDirectory
};
} catch (error) {
this.logService.warn(`Failed to load session ${metadata.sessionId}: ${error}`);
}
})
));
const diskSessionIds = new Set(diskSessions.map(s => s.id));
// If we have a new session that has started, then return that as well.
// Possible SDK has not yet persisted it to disk.
const newSessions = coalesce(await Promise.all(Array.from(this._sessionWrappers.values())
.filter(session => !diskSessionIds.has(session.object.sessionId))
.filter(session => session.object.status === ChatSessionStatus.InProgress)
.map(async (session): Promise<ICopilotCLISessionItem | undefined> => {
const label = await this.customSessionTitleService.getCustomSessionTitle(session.object.sessionId) ?? labelFromPrompt(session.object.pendingPrompt ?? '');
if (!label) {
return;
}
const createTime = Date.now();
return {
id: session.object.sessionId,
label,
status: session.object.status,
timing: { created: createTime, startTime: createTime },
};
})));
// Merge with cached sessions (new sessions not yet persisted by SDK)
const allSessions = diskSessions
.map((session): ICopilotCLISessionItem => {
return {
...session,
status: this._sessionWrappers.get(session.id)?.object?.status
};
}).concat(newSessions);
allSessions.forEach(session => this._cachedSessionItems.set(session.id, session));
return allSessions;
} catch (error) {
this.logService.error(`Failed to get all sessions: ${error}`);
throw error;
} finally {
this._isGettingSessions--;
}
}
private async constructSessionItem(metadata: LocalSessionMetadata, token: CancellationToken): Promise<ICopilotCLISessionItem | undefined> {
const sessionItem = await this.constructSessionItemImpl(metadata, token);
if (sessionItem) {
this._cachedSessionItems.set(metadata.sessionId, sessionItem);
}
return sessionItem;
}
private async constructSessionItemFromWrappedSession(session: RefCountedSession, token: CancellationToken): Promise<ICopilotCLISessionItem | undefined> {
const label = (await this.getSessionTitle(session.object.sessionId, token)) || this._cachedSessionItems.get(session.object.sessionId)?.label || labelFromPrompt(session.object.pendingPrompt ?? '');
const createTime = Date.now();
return {
id: session.object.sessionId,
label,
status: session.object.status,
timing: this._cachedSessionItems.get(session.object.sessionId)?.timing ?? { created: createTime, startTime: createTime },
};
}
private async constructSessionItemImpl(metadata: LocalSessionMetadata, token: CancellationToken): Promise<ICopilotCLISessionItem | undefined> {
const workingDirectory = metadata.context?.cwd ? URI.file(metadata.context.cwd) : undefined;
this._sessionWorkingDirectories.set(metadata.sessionId, workingDirectory);
const shouldShowSession = await this.shouldShowSession(metadata.sessionId, metadata.context);
if (!shouldShowSession) {
return undefined;
}
const id = metadata.sessionId;
const startTime = metadata.startTime.getTime();
const endTime = metadata.modifiedTime.getTime();
const label = await this.getSessionTitleImpl(metadata.sessionId, metadata, token) ?? labelFromPrompt(metadata.summary ?? '');
if (label) {
return {
id,
label,
timing: { created: startTime, startTime, endTime },
workingDirectory,
status: this._sessionWrappers.get(id)?.object?.status
};
}
}
public async createSession(options: ICreateSessionOptions, token: CancellationToken): Promise<RefCountedSession> {
const resource = options.sessionId ? SessionIdForCLI.getResource(options.sessionId) : URI.from({ scheme: 'copilot-cli', path: `mcp-gateway-${generateUuid()}` });
const { mcpConfig: mcpServers, disposable: mcpGateway } = await this.mcpHandler.loadMcpConfig(resource);
try {
const sessionOptions = await this.createSessionsOptions({ ...options, mcpServers });
const sessionManager = await raceCancellationError(this.getSessionManager(), token);
const sdkSession = await sessionManager.createSession({ ...sessionOptions, sessionId: options.sessionId });
this._newSessionIds.delete(sdkSession.sessionId);
// After the first session creation, the SDK's OTel TracerProvider is
// initialized. Install the bridge processor so SDK-native spans flow
// to the debug panel.
this._installBridgeIfNeeded();
if (sessionOptions.copilotUrl) {
sdkSession.setAuthInfo({
type: 'hmac',
hmac: 'empty',
host: 'https://github.com',
copilotUser: {
endpoints: {
api: sessionOptions.copilotUrl
}
}
});
}
this.logService.trace(`[CopilotCLISession] Created new CopilotCLI session ${sdkSession.sessionId}.`);
const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager);
session.object.add(mcpGateway);
return session;
}
catch (error) {
mcpGateway.dispose();
throw error;
}
}
/**
* Install the bridge SpanProcessor on the SDK's global TracerProvider.
* Called once after the first session creation (when the SDK provider is ready).
*/
private _installBridgeIfNeeded(): void {
if (this._bridgeInstalled) {
return;
}
this._bridgeInstalled = true;
try {
// The SDK registered its BasicTracerProvider as the global provider.
// In OTel SDK v2, addSpanProcessor() was removed from BasicTracerProvider.
// We access the internal MultiSpanProcessor._spanProcessors array to inject
// our bridge. This is the same pattern the SDK itself uses in forceFlush().
const api = require('@opentelemetry/api') as typeof import('@opentelemetry/api');
const globalProvider = api.trace.getTracerProvider();
// Navigate: ProxyTracerProvider._delegate → BasicTracerProvider._activeSpanProcessor → MultiSpanProcessor._spanProcessors
const delegate = (globalProvider as unknown as Record<string, unknown>)._delegate ?? globalProvider;
const activeProcessor = (delegate as unknown as Record<string, unknown>)._activeSpanProcessor as Record<string, unknown> | undefined;
const processorArray = activeProcessor?._spanProcessors;
if (Array.isArray(processorArray)) {
this._bridgeProcessor = new CopilotCliBridgeSpanProcessor(this._otelService);
processorArray.push(this._bridgeProcessor);
this.logService.info('[CopilotCLISession] Bridge SpanProcessor installed on SDK TracerProvider');
} else {
this.logService.warn('[CopilotCLISession] Could not access SDK TracerProvider internals — debug panel will not show SDK spans');
}
} catch (err) {
this.logService.warn(`[CopilotCLISession] Failed to install bridge SpanProcessor: ${err}`);
}
}
private async shouldShowSession(sessionId: string, context?: SessionContext): Promise<boolean> {
if (isUntitledSessionId(sessionId)) {
return true;
}
// If we're in an empty workspace then show all sessions.
if (this.workspaceService.getWorkspaceFolders().length === 0) {
return true;
}
if (this._agentSessionsWorkspace.isAgentSessionsWorkspace) {
return true;
}
// This session was started from a specified workspace (e.g. multiroot, untitled or other), hence continue showing it.
const sessionTrackerVisibility = this._sessionTracker.shouldShowSession(sessionId);
if (sessionTrackerVisibility.isWorkspaceSession) {
return true;
}
// Possible we have the workspace info in cli metadata.
if (context && (
(context.cwd && this.workspaceService.getWorkspaceFolder(URI.file(context.cwd))) ||
(context.gitRoot && this.workspaceService.getWorkspaceFolder(URI.file(context.gitRoot)))
)) {
return true;
}
// If we have a workspace folder for this and the workspace folder belongs to one of the open workspace folders, show it.
const workspaceFolder = await this.workspaceFolderService.getSessionWorkspaceFolder(sessionId);
if (workspaceFolder && this.workspaceService.getWorkspaceFolder(workspaceFolder)) {
return true;
}
// If we have a git worktree and the worktree's repo belongs to one of the workspace folders, show it.
const worktree = await this.worktreeManager.getWorktreeProperties(sessionId);
if (worktree && this.workspaceService.getWorkspaceFolder(URI.file(worktree.repositoryPath))) {
return true;
}
// If this is an old global session, show it if we don't have specific data to exclude it.
if (sessionTrackerVisibility.isOldGlobalSession && !workspaceFolder && !worktree && (this.workspaceService.getWorkspaceFolders().length === 0 || this._agentSessionsWorkspace.isAgentSessionsWorkspace)) {
return true;
}
return false;
}
protected async createSessionsOptions(options: ICreateSessionOptions & { mcpServers?: SessionOptions['mcpServers'] }): Promise<Readonly<SessionOptions>> {
const [agentInfos, skillLocations] = await Promise.all([
this.agents.getAgents(),
this.copilotCLISkills.getSkillsLocations(CancellationToken.None),
]);
const customAgents = agentInfos.map(i => i.agent);
const variablesContext = this._promptVariablesService.buildTemplateVariablesContext(options.sessionId, options.debugTargetSessionIds);
const systemMessage = variablesContext ? { mode: 'append' as const, content: variablesContext } : undefined;
const allOptions: SessionOptions = {
clientName: 'vscode',
};
const workingDirectory = getWorkingDirectory(options.workspace);
if (workingDirectory) {
allOptions.workingDirectory = workingDirectory.fsPath;
}
if (options.model) {
allOptions.model = options.model as unknown as SessionOptions['model'];
}
if (options.mcpServers && Object.keys(options.mcpServers).length > 0) {
allOptions.mcpServers = options.mcpServers;
this.logService.info(`[CopilotCLISession] Passing ${Object.keys(options.mcpServers).length} MCP server(s) to SDK: [${Object.keys(options.mcpServers).join(', ')}]`);
for (const [id, cfg] of Object.entries(options.mcpServers)) {
this.logService.info(`[CopilotCLISession] ${id}: type=${cfg.type}`);
}
} else {
this.logService.info('[CopilotCLISession] No MCP servers to pass to SDK');
}
if (skillLocations.length > 0) {
allOptions.skillDirectories = skillLocations.map(uri => uri.fsPath);
}
if (options.mcpServerMappings?.size && customAgents && options.mcpServers) {
remapCustomAgentTools(customAgents, options.mcpServerMappings, options.mcpServers, options.agent);
}
if (options.agent) {
allOptions.selectedCustomAgent = options.agent;
}
if (customAgents.length > 0) {
allOptions.customAgents = customAgents;
}
allOptions.enableStreaming = true;
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
if (copilotUrl) {
allOptions.copilotUrl = copilotUrl;
}
if (systemMessage) {
allOptions.systemMessage = systemMessage;
}
allOptions.sessionCapabilities = new Set(['plan-mode', 'memory', 'cli-documentation', 'ask-user', 'interactive-mode', 'system-notifications']);
if (options.reasoningEffort && this.configurationService.getConfig(ConfigKey.Advanced.CLIThinkingEffortEnabled)) {
allOptions.reasoningEffort = options.reasoningEffort;
}
return allOptions as Readonly<SessionOptions>;
}
public async getSession(options: IGetSessionOptions, token: CancellationToken): Promise<RefCountedSession | undefined> {
// https://github.com/microsoft/vscode/issues/276573
const lock = this.sessionMutexForGetSession.get(options.sessionId) ?? new Mutex();
this.sessionMutexForGetSession.set(options.sessionId, lock);
const lockDisposable = await lock.acquire(token);
try {
{
const session = this._sessionWrappers.get(options.sessionId);
if (session) {
this.logService.trace(`[CopilotCLISession] Reusing CopilotCLI session ${options.sessionId}.`);
this._partialSessionHistories.delete(options.sessionId);
session.acquire();
if (options.agent) {
await session.object.sdkSession.selectCustomAgent(options.agent.name);
} else {
session.object.sdkSession.clearCustomAgent();
}
return session;
}
}
const [sessionManager, { mcpConfig: mcpServers, disposable: mcpGateway }] = await Promise.all([
raceCancellationError(this.getSessionManager(), token),
this.mcpHandler.loadMcpConfig(SessionIdForCLI.getResource(options.sessionId)),
]);
try {
const sessionOptions = await this.createSessionsOptions({ ...options, mcpServers });
const sdkSession = await sessionManager.getSession({ ...sessionOptions, sessionId: options.sessionId }, true);
if (!sdkSession) {
this.logService.error(`[CopilotCLISession] CopilotCLI failed to get session ${options.sessionId}.`);
return undefined;
}
const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager);
session.object.add(mcpGateway);
return session;
}
catch (error) {
mcpGateway.dispose();
throw error;
}
} finally {
lockDisposable?.dispose();
}
}
public async getChatHistory({ sessionId, workspace }: { sessionId: string; workspace: IWorkspaceInfo }, token: CancellationToken): Promise<(ChatRequestTurn2 | ChatResponseTurn2)[]> {
const { history } = await this.getChatHistoryImpl({ sessionId, workspace }, token);
return history;
}
private async getChatHistoryImpl({ sessionId, workspace }: { sessionId: string; workspace: IWorkspaceInfo }, token: CancellationToken): Promise<{ history: (ChatRequestTurn2 | ChatResponseTurn2)[]; events: readonly SessionEvent[] }> {
const requestDetailsPromise = this._chatSessionMetadataStore.getRequestDetails(sessionId);
const agentIdPromise = this._chatSessionMetadataStore.getSessionAgent(sessionId);
const sessionManager = await raceCancellation(this.getSessionManager(), token);
if (!sessionManager || token.isCancellationRequested) {
requestDetailsPromise.catch(error => {/** */ });
agentIdPromise.catch(error => {/** */ });
return { history: [], events: [] };
}
let events: readonly SessionEvent[] = [];
let modelId: string | undefined = undefined;
// Try to shutdown session as soon as possible.
const existingSession = this._sessionWrappers.get(sessionId)?.object?.sdkSession;
if (existingSession) {
modelId = await existingSession.getSelectedModel();
events = existingSession.getEvents();
} else {
let shutdown = Promise.resolve();
try {
const session = await sessionManager.getSession({ sessionId }, false);
if (!session) {
return { history: [], events: [] };
}
modelId = await session.getSelectedModel();
events = session.getEvents();
shutdown = sessionManager.closeSession(sessionId).catch(error => {
this.logService.error(`[CopilotCLISession] Failed to close session ${sessionId} after fetching chat history: ${error}`);
});
} finally {
await shutdown;
}
}
const [agentId, storedDetails] = await Promise.all([agentIdPromise, requestDetailsPromise]);
// Build lookup from copilotRequestId → RequestDetails for the callback
const customAgentLookup = await this.createCustomAgentLookup();
const legacyMappings: RequestDetails[] = [];
const detailsByCopilotId = new Map<string, RequestIdDetails>();
const defaultModeInstructions = agentId ? await this.resolveAgentModeInstructions(agentId, customAgentLookup) : undefined;
for (const d of storedDetails) {
if (d.copilotRequestId) {
const modeInstructions = d.modeInstructions ?? await this.resolveAgentModeInstructions(d.agentId, customAgentLookup) ?? defaultModeInstructions;
detailsByCopilotId.set(d.copilotRequestId, { requestId: d.vscodeRequestId, toolIdEditMap: d.toolIdEditMap, modeInstructions });
}
}
const getVSCodeRequestId = (sdkRequestId: string) => {
const stored = detailsByCopilotId.get(sdkRequestId);
if (stored) {
return stored;
}
const mapping = this.copilotCLISDK.getRequestId(sdkRequestId);
if (mapping) {
detailsByCopilotId.set(sdkRequestId, mapping);
legacyMappings.push({
copilotRequestId: sdkRequestId,
vscodeRequestId: mapping.requestId,
toolIdEditMap: mapping.toolIdEditMap,
});
}
return mapping;
};
const history = buildChatHistoryFromEvents(sessionId, modelId, events, getVSCodeRequestId, this._delegationSummaryService, this.logService, getWorkingDirectory(workspace), defaultModeInstructions);
if (legacyMappings.length > 0) {
void this._chatSessionMetadataStore.updateRequestDetails(sessionId, legacyMappings).catch(error => {
this.logService.error(`[CopilotCLISession] Failed to update chat session metadata store with legacy mappings for session ${sessionId}`, error);
});
}
return { history, events };
}
private async createCustomAgentLookup(): Promise<Map<string, [ChatCustomAgent, Lazy<Promise<string>>]>> {
const agents = await this._promptsService.getCustomAgents(CancellationToken.None);
const lookup = new Map<string, [ChatCustomAgent, Lazy<Promise<string>>]>();
for (const agent of agents) {
const lazyContent = new Lazy(() => this._promptsService.parseFile(agent.uri, CancellationToken.None).then(parsed => parsed.body?.getContent() ?? ''));
const keys = [
agent.name?.trim(),
agent.uri.toString(),
getAgentFileNameFromFilePath(agent.uri),
];
for (const key of keys) {
if (key && !lookup.has(key)) {
lookup.set(key, [agent, lazyContent]);
}
}
}
return lookup;
}
private async resolveAgentModeInstructions(agentId: string | undefined, customAgentLookup: Map<string, [ChatCustomAgent, Lazy<Promise<string>>]>): Promise<StoredModeInstructions | undefined> {
if (!agentId) {
return undefined;
}
const agentEntry = customAgentLookup.get(agentId);
if (!agentEntry) {
return undefined;
}
const [agent, lazyContent] = agentEntry;
return {
uri: agent.uri.toString(),
name: agent.name?.trim() || agentId,
content: await lazyContent.value,
};
}
/**
* Fork an existing session using the SDK's `forkSession` API.
*
* The SDK handles copying the event log and (optionally) truncating to a boundary event.
* This method additionally stores VS Code-specific workspace metadata and custom title.
*
* Returns the id of the forked session.
*/
public async forkSession({ sessionId, requestId, workspace }: { sessionId: string; requestId: string | undefined; workspace: IWorkspaceInfo }, token: CancellationToken): Promise<string> {
// Resolve the SDK event ID boundary for truncation BEFORE forking.
// We need the source session's history and request details to translate the VS Code requestId
// into the SDK event ID that the SDK's forkSession accepts.
const [sessionManager, title, { history, events: originalSessionEvents }] = await Promise.all([
raceCancellationError(this.getSessionManager(), token),
this.getSessionTitle(sessionId, token),
requestId ? this.getChatHistoryImpl({ sessionId, workspace }, token) : Promise.resolve({ history: [], events: [] }),
]);
let toEventId: string | undefined;
if (requestId) {
const requestToTruncateTo = history.find(event => event instanceof ChatRequestTurn2 && event.id === requestId);
if (requestToTruncateTo) {
const storedDetails = await this._chatSessionMetadataStore.getRequestDetails(sessionId);
const translatedSDKEvent = storedDetails.find(d => d.vscodeRequestId === requestToTruncateTo.id || d.copilotRequestId === requestToTruncateTo.id)?.copilotRequestId;
const sdkEvent = originalSessionEvents.find(e => e.type === 'user.message' && e.id === requestToTruncateTo.id)?.id;
toEventId = translatedSDKEvent ?? sdkEvent;
if (!toEventId) {
this.logService.warn(`[CopilotCLISession] Cannot find SDK event id for request id ${requestId} in session ${sessionId}. Will fork without truncation.`);
}
} else {
this.logService.warn(`[CopilotCLISession] Failed to find request ${requestId} in session ${sessionId} history. Will fork without truncation.`);
}
}
const { sessionId: newSessionId } = await sessionManager.forkSession(sessionId, toEventId);
this._sessionsBeingCreatedViaFork.add(newSessionId);
try {
const forkedTitlePrefix = l10n.t("Forked: ");
const customTitle = title.startsWith(forkedTitlePrefix) ? title : l10n.t("Forked: {0}", title);
await this._chatSessionMetadataStore.storeForkedSessionMetadata(sessionId, newSessionId, customTitle);
this._onDidChangeSessions.fire();
this._onDidCreateSession.fire({
id: newSessionId,
label: customTitle,
timing: { created: Date.now(), startTime: Date.now() },
workingDirectory: getWorkingDirectory(workspace)
});
return newSessionId;
} finally {
this._sessionsBeingCreatedViaFork.delete(newSessionId);
}
}
/**
* Fork an existing session by creating a new session id and copying the underlying
* Copilot CLI session workspace and metadata.
*
* High-level algorithm:
* 1. Copy the existing session folder (and related files) into a new folder for the new session id.
* 2. Update any session metadata so it references the new session id instead of the original.
* 3. Open the new session and truncate it to the last event id to ensure the event log is consistent.
* 4. Close and reopen the new session (via `getSession`) so in-memory state reflects the updated data.
*
* Returns the id of the forked session.
*
* @deprecated Use `forkSession` which delegates to the SDK's `forkSession` API.
*/
public async forkSessionV1({ sessionId, requestId, workspace }: { sessionId: string; requestId: string | undefined; workspace: IWorkspaceInfo }, token: CancellationToken): Promise<string> {
const newSessionId = generateUuid();
this._sessionsBeingCreatedViaFork.add(newSessionId);
try {
const [sessionManager, title, { history, events: originalSessionEvents }, sessionOptions] = await Promise.all([
raceCancellationError(this.getSessionManager(), token),
this.getSessionTitle(sessionId, token),
requestId ? this.getChatHistoryImpl({ sessionId, workspace }, token) : Promise.resolve({ history: [], events: [] }),
this.createSessionsOptions({ workspace, mcpServers: undefined, agent: undefined, sessionId: newSessionId }),
copySessionFilesForForking(sessionId, newSessionId, workspace, this._chatSessionMetadataStore, token),
]);
const session = await sessionManager.getSession({ ...sessionOptions, sessionId: newSessionId }, false);
if (!session) {
this.logService.error(`[CopilotCLISession] CopilotCLI failed to open forked session ${newSessionId}.`);
throw new Error(`Failed to fork session ${sessionId}`);
}
const forkedTitlePrefix = l10n.t("Forked: ");
const customTitle = title.startsWith(forkedTitlePrefix) ? title : l10n.t("Forked: {0}", title);
const customTitlePromise = this.customSessionTitleService.setCustomSessionTitle(newSessionId, customTitle);
// Only if we have a request to truncate should we open and trucate.
if (requestId) {
const requestToTruncateTo = history.find(event => event instanceof ChatRequestTurn2 && event.id === requestId);
if (requestToTruncateTo) {
const requestId = requestToTruncateTo.id;
const storedDetails = await this._chatSessionMetadataStore.getRequestDetails(newSessionId);
const translatedSDKEvent = storedDetails.find(d => d.vscodeRequestId === requestId || d.copilotRequestId === requestId)?.copilotRequestId;
const sdkEvent = originalSessionEvents.find(e => e.type === 'user.message' && e.id === requestId)?.id;
const eventToTruncateTo = translatedSDKEvent ?? sdkEvent;
if (eventToTruncateTo) {
await session.truncateToEvent(eventToTruncateTo);
const events = session.getEvents();
const eventsFile = Uri.file(getCopilotCLISessionEventsFile(newSessionId));
// File must end with EOL
const contents = Buffer.from(events.map(e => JSON.stringify(e)).join(EOL) + EOL);
await this.fileSystem.writeFile(eventsFile, contents);
} else {
this.logService.warn(`[CopilotCLISession] Cannot find event id to truncate to for request id ${requestId} in session ${newSessionId}`);
}
} else {
this.logService.warn(`[CopilotCLISession] Failed to find event id ${requestId} in session ${newSessionId} while forking. Will not truncate the session.`);
}
}
await Promise.all([sessionManager.closeSession(newSessionId), customTitlePromise]);
this._onDidChangeSessions.fire();
this._onDidCreateSession.fire({
id: newSessionId,
label: customTitle,
timing: { created: Date.now(), startTime: Date.now() },
workingDirectory: getWorkingDirectory(workspace)
});