mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Merge branch 'main' into tyriar/284593_npm_spec__actual_spec
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1677
extensions/terminal-suggest/src/completions/yarn.ts
Normal file
1677
extensions/terminal-suggest/src/completions/yarn.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -114,8 +114,6 @@ export const upstreamSpecs = [
|
||||
// JavaScript / TypeScript
|
||||
'node',
|
||||
'nvm',
|
||||
'pnpm',
|
||||
'yarn',
|
||||
'yo',
|
||||
|
||||
// Python
|
||||
|
||||
@@ -18,7 +18,9 @@ import gitCompletionSpec from './completions/git';
|
||||
import ghCompletionSpec from './completions/gh';
|
||||
import npmCompletionSpec from './completions/npm';
|
||||
import npxCompletionSpec from './completions/npx';
|
||||
import pnpmCompletionSpec from './completions/pnpm';
|
||||
import setLocationSpec from './completions/set-location';
|
||||
import yarnCompletionSpec from './completions/yarn';
|
||||
import { upstreamSpecs } from './constants';
|
||||
import { ITerminalEnvironment, PathExecutableCache } from './env/pathExecutableCache';
|
||||
import { executeCommand, executeCommandTimeout, IFigExecuteExternals } from './fig/execute';
|
||||
@@ -72,7 +74,9 @@ export const availableSpecs: Fig.Spec[] = [
|
||||
ghCompletionSpec,
|
||||
npmCompletionSpec,
|
||||
npxCompletionSpec,
|
||||
pnpmCompletionSpec,
|
||||
setLocationSpec,
|
||||
yarnCompletionSpec,
|
||||
];
|
||||
for (const spec of upstreamSpecs) {
|
||||
availableSpecs.push(require(`./completions/upstream/${spec}`).default);
|
||||
|
||||
@@ -148,7 +148,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
|
||||
this._agents.deleteAndDispose(handle);
|
||||
}
|
||||
|
||||
$transferActiveChatSession(toWorkspace: UriComponents): void {
|
||||
async $transferActiveChatSession(toWorkspace: UriComponents): Promise<void> {
|
||||
const widget = this._chatWidgetService.lastFocusedWidget;
|
||||
const model = widget?.viewModel?.model;
|
||||
if (!model) {
|
||||
@@ -156,8 +156,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
|
||||
return;
|
||||
}
|
||||
|
||||
const location = widget.location;
|
||||
this._chatService.transferChatSession({ sessionId: model.sessionId, inputState: model.inputModel.state.get(), location }, URI.revive(toWorkspace));
|
||||
await this._chatService.transferChatSession(model.sessionResource, URI.revive(toWorkspace));
|
||||
}
|
||||
|
||||
async $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): Promise<void> {
|
||||
|
||||
@@ -1469,7 +1469,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
|
||||
// namespace: interactive
|
||||
const interactive: typeof vscode.interactive = {
|
||||
transferActiveChat(toWorkspace: vscode.Uri) {
|
||||
transferActiveChat(toWorkspace: vscode.Uri): Thenable<void> {
|
||||
checkProposedApiEnabled(extension, 'interactive');
|
||||
return extHostChatAgents2.transferActiveChat(toWorkspace);
|
||||
}
|
||||
|
||||
@@ -1402,7 +1402,7 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi
|
||||
$updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void;
|
||||
$unregisterAgent(handle: number): void;
|
||||
|
||||
$transferActiveChatSession(toWorkspace: UriComponents): void;
|
||||
$transferActiveChatSession(toWorkspace: UriComponents): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ICodeMapperTextEdit {
|
||||
|
||||
@@ -437,8 +437,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
});
|
||||
}
|
||||
|
||||
transferActiveChat(newWorkspace: vscode.Uri): void {
|
||||
this._proxy.$transferActiveChatSession(newWorkspace);
|
||||
async transferActiveChat(newWorkspace: vscode.Uri): Promise<void> {
|
||||
await this._proxy.$transferActiveChatSession(newWorkspace);
|
||||
}
|
||||
|
||||
createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant {
|
||||
|
||||
@@ -310,13 +310,15 @@ abstract class OpenChatGlobalAction extends Action2 {
|
||||
let resp: Promise<IChatResponseModel | undefined> | undefined;
|
||||
|
||||
if (opts?.query) {
|
||||
chatWidget.setInput(opts.query);
|
||||
|
||||
if (!opts.isPartialQuery) {
|
||||
if (opts.isPartialQuery) {
|
||||
chatWidget.setInput(opts.query);
|
||||
} else {
|
||||
if (!chatWidget.viewModel) {
|
||||
await Event.toPromise(chatWidget.onDidChangeViewModel);
|
||||
}
|
||||
await waitForDefaultAgent(chatAgentService, chatWidget.input.currentModeKind);
|
||||
chatWidget.setInput(opts.query); // wait until the model is restored before setting the input, or it will be cleared when the model is restored
|
||||
resp = chatWidget.acceptInput();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,9 +215,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
|
||||
private onDidChangeAgents(): void {
|
||||
if (this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat)) {
|
||||
if (!this._widget?.viewModel && !this.restoringSession) {
|
||||
const info = this.getTransferredOrPersistedSessionInfo();
|
||||
const sessionResource = this.getTransferredOrPersistedSessionInfo();
|
||||
this.restoringSession =
|
||||
(info.sessionId ? this.chatService.getOrRestoreSession(LocalChatSessionUri.forSession(info.sessionId)) : Promise.resolve(undefined)).then(async modelRef => {
|
||||
(sessionResource ? this.chatService.getOrRestoreSession(sessionResource) : Promise.resolve(undefined)).then(async modelRef => {
|
||||
if (!this._widget) {
|
||||
return; // renderBody has not been called yet
|
||||
}
|
||||
@@ -228,9 +228,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
|
||||
const wasVisible = this._widget.visible;
|
||||
try {
|
||||
this._widget.setVisible(false);
|
||||
if (info.inputState && modelRef) {
|
||||
modelRef.object.inputModel.setState(info.inputState);
|
||||
}
|
||||
|
||||
await this.showModel(modelRef);
|
||||
} finally {
|
||||
@@ -245,16 +242,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
|
||||
this._onDidChangeViewWelcomeState.fire();
|
||||
}
|
||||
|
||||
private getTransferredOrPersistedSessionInfo(): { sessionId?: string; inputState?: IChatModelInputState; mode?: ChatModeKind } {
|
||||
if (this.chatService.transferredSessionData?.location === ChatAgentLocation.Chat) {
|
||||
const sessionId = this.chatService.transferredSessionData.sessionId;
|
||||
return {
|
||||
sessionId,
|
||||
inputState: this.chatService.transferredSessionData.inputState,
|
||||
};
|
||||
private getTransferredOrPersistedSessionInfo(): URI | undefined {
|
||||
if (this.chatService.transferredSessionResource) {
|
||||
return this.chatService.transferredSessionResource;
|
||||
}
|
||||
|
||||
return { sessionId: this.viewState.sessionId };
|
||||
return this.viewState.sessionId ? LocalChatSessionUri.forSession(this.viewState.sessionId) : undefined;
|
||||
}
|
||||
|
||||
protected override renderBody(parent: HTMLElement): void {
|
||||
@@ -658,12 +651,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
|
||||
//#region Model Management
|
||||
|
||||
private async applyModel(): Promise<void> {
|
||||
const info = this.getTransferredOrPersistedSessionInfo();
|
||||
const modelRef = info.sessionId ? await this.chatService.getOrRestoreSession(LocalChatSessionUri.forSession(info.sessionId)) : undefined;
|
||||
if (modelRef && info.inputState) {
|
||||
modelRef.object.inputModel.setState(info.inputState);
|
||||
}
|
||||
|
||||
const sessionResource = this.getTransferredOrPersistedSessionInfo();
|
||||
const modelRef = sessionResource ? await this.chatService.getOrRestoreSession(sessionResource) : undefined;
|
||||
await this.showModel(modelRef);
|
||||
}
|
||||
|
||||
@@ -673,8 +662,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
|
||||
|
||||
let ref: IChatModelReference | undefined;
|
||||
if (startNewSession) {
|
||||
ref = modelRef ?? (this.chatService.transferredSessionData?.sessionId && this.chatService.transferredSessionData?.location === ChatAgentLocation.Chat
|
||||
? await this.chatService.getOrRestoreSession(LocalChatSessionUri.forSession(this.chatService.transferredSessionData.sessionId))
|
||||
ref = modelRef ?? (this.chatService.transferredSessionResource
|
||||
? await this.chatService.getOrRestoreSession(this.chatService.transferredSessionResource)
|
||||
: this.chatService.startSession(ChatAgentLocation.Chat));
|
||||
if (!ref) {
|
||||
throw new Error('Could not start chat session');
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ICellEditOperation } from '../../notebook/common/notebookCommon.js';
|
||||
import { IWorkspaceSymbol } from '../../search/common/search.js';
|
||||
import { IChatAgentCommand, IChatAgentData, IChatAgentResult, UserSelectedTools } from './chatAgents.js';
|
||||
import { IChatEditingSession } from './chatEditingService.js';
|
||||
import { IChatModel, IChatModelInputState, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js';
|
||||
import { IChatModel, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js';
|
||||
import { IParsedChatRequest } from './chatParserTypes.js';
|
||||
import { IChatParserContext } from './chatRequestParser.js';
|
||||
import { IChatRequestVariableEntry } from './chatVariableEntries.js';
|
||||
@@ -934,12 +934,6 @@ export interface IChatProviderInfo {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface IChatTransferredSessionData {
|
||||
sessionId: string;
|
||||
location: ChatAgentLocation;
|
||||
inputState: IChatModelInputState | undefined;
|
||||
}
|
||||
|
||||
export interface IChatSendRequestResponseState {
|
||||
responseCreatedPromise: Promise<IChatResponseModel>;
|
||||
responseCompletePromise: Promise<void>;
|
||||
@@ -1006,7 +1000,7 @@ export const IChatService = createDecorator<IChatService>('IChatService');
|
||||
|
||||
export interface IChatService {
|
||||
_serviceBrand: undefined;
|
||||
transferredSessionData: IChatTransferredSessionData | undefined;
|
||||
transferredSessionResource: URI | undefined;
|
||||
|
||||
readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }>;
|
||||
|
||||
@@ -1066,7 +1060,7 @@ export interface IChatService {
|
||||
notifyUserAction(event: IChatUserActionEvent): void;
|
||||
readonly onDidDisposeSession: Event<{ readonly sessionResource: URI[]; readonly reason: 'cleared' }>;
|
||||
|
||||
transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void;
|
||||
transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise<void>;
|
||||
|
||||
activateDefaultAgent(location: ChatAgentLocation): Promise<void>;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Disposable, DisposableResourceMap, DisposableStore, IDisposable, Mutabl
|
||||
import { revive } from '../../../../base/common/marshalling.js';
|
||||
import { Schemas } from '../../../../base/common/network.js';
|
||||
import { autorun, derived, IObservable } from '../../../../base/common/observable.js';
|
||||
import { isEqual } from '../../../../base/common/resources.js';
|
||||
import { StopWatch } from '../../../../base/common/stopwatch.js';
|
||||
import { isDefined } from '../../../../base/common/types.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
@@ -24,7 +25,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { Progress } from '../../../../platform/progress/common/progress.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
|
||||
import { InlineChatConfigKeys } from '../../inlineChat/common/inlineChat.js';
|
||||
@@ -36,10 +37,10 @@ import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, ICha
|
||||
import { ChatModelStore, IStartSessionProps } from './chatModelStore.js';
|
||||
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from './chatParserTypes.js';
|
||||
import { ChatRequestParser } from './chatRequestParser.js';
|
||||
import { ChatMcpServersStarting, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatTransferredSessionData, IChatUserActionEvent, ResponseModelState } from './chatService.js';
|
||||
import { ChatMcpServersStarting, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js';
|
||||
import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js';
|
||||
import { IChatSessionsService } from './chatSessionsService.js';
|
||||
import { ChatSessionStore, IChatSessionEntryMetadata, IChatTransfer2 } from './chatSessionStore.js';
|
||||
import { ChatSessionStore, IChatSessionEntryMetadata } from './chatSessionStore.js';
|
||||
import { IChatSlashCommandService } from './chatSlashCommands.js';
|
||||
import { IChatTransferService } from './chatTransferService.js';
|
||||
import { LocalChatSessionUri } from './chatUri.js';
|
||||
@@ -50,10 +51,6 @@ import { ILanguageModelToolsService } from './languageModelToolsService.js';
|
||||
|
||||
const serializedChatKey = 'interactive.sessions';
|
||||
|
||||
const TransferredGlobalChatKey = 'chat.workspaceTransfer';
|
||||
|
||||
const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60;
|
||||
|
||||
class CancellableRequest implements IDisposable {
|
||||
constructor(
|
||||
public readonly cancellationTokenSource: CancellationTokenSource,
|
||||
@@ -82,9 +79,9 @@ export class ChatService extends Disposable implements IChatService {
|
||||
private _persistedSessions: ISerializableChatsData;
|
||||
private _saveModelsEnabled = true;
|
||||
|
||||
private _transferredSessionData: IChatTransferredSessionData | undefined;
|
||||
public get transferredSessionData(): IChatTransferredSessionData | undefined {
|
||||
return this._transferredSessionData;
|
||||
private _transferredSessionResource: URI | undefined;
|
||||
public get transferredSessionResource(): URI | undefined {
|
||||
return this._transferredSessionResource;
|
||||
}
|
||||
|
||||
private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI }>());
|
||||
@@ -128,7 +125,7 @@ export class ChatService extends Disposable implements IChatService {
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@@ -175,21 +172,15 @@ export class ChatService extends Disposable implements IChatService {
|
||||
this._persistedSessions = {};
|
||||
}
|
||||
|
||||
const transferredData = this.getTransferredSessionData();
|
||||
const transferredChat = transferredData?.chat;
|
||||
if (transferredChat) {
|
||||
this.trace('constructor', `Transferred session ${transferredChat.sessionId}`);
|
||||
this._persistedSessions[transferredChat.sessionId] = transferredChat;
|
||||
this._transferredSessionData = {
|
||||
sessionId: transferredChat.sessionId,
|
||||
location: transferredData.location,
|
||||
inputState: transferredData.inputState
|
||||
};
|
||||
}
|
||||
|
||||
this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore));
|
||||
this._chatSessionStore.migrateDataIfNeeded(() => this._persistedSessions);
|
||||
|
||||
const transferredData = this._chatSessionStore.getTransferredSessionData();
|
||||
if (transferredData) {
|
||||
this.trace('constructor', `Transferred session ${transferredData}`);
|
||||
this._transferredSessionResource = transferredData;
|
||||
}
|
||||
|
||||
// When using file storage, populate _persistedSessions with session metadata from the index
|
||||
// This ensures that getPersistedSessionTitle() can find titles for inactive sessions
|
||||
this.initializePersistedSessionsFromFileStorage().then(() => {
|
||||
@@ -309,23 +300,6 @@ export class ChatService extends Disposable implements IChatService {
|
||||
}
|
||||
}
|
||||
|
||||
private getTransferredSessionData(): IChatTransfer2 | undefined {
|
||||
const data: IChatTransfer2[] = this.storageService.getObject(TransferredGlobalChatKey, StorageScope.PROFILE, []);
|
||||
const workspaceUri = this.workspaceContextService.getWorkspace().folders[0]?.uri;
|
||||
if (!workspaceUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thisWorkspace = workspaceUri.toString();
|
||||
const currentTime = Date.now();
|
||||
// Only use transferred data if it was created recently
|
||||
const transferred = data.find(item => URI.revive(item.toWorkspace).toString() === thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS));
|
||||
// Keep data that isn't for the current workspace and that hasn't expired yet
|
||||
const filtered = data.filter(item => URI.revive(item.toWorkspace).toString() !== thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS));
|
||||
this.storageService.store(TransferredGlobalChatKey, JSON.stringify(filtered), StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
return transferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* todo@connor4312 This will be cleaned up with the globalization of edits.
|
||||
*/
|
||||
@@ -540,8 +514,9 @@ export class ChatService extends Disposable implements IChatService {
|
||||
}
|
||||
|
||||
let sessionData: ISerializableChatData | undefined;
|
||||
if (this.transferredSessionData?.sessionId === sessionId) {
|
||||
sessionData = revive(this._persistedSessions[sessionId]);
|
||||
if (isEqual(this.transferredSessionResource, sessionResource)) {
|
||||
this._transferredSessionResource = undefined;
|
||||
sessionData = revive(await this._chatSessionStore.readTransferredSession(sessionResource));
|
||||
} else {
|
||||
sessionData = revive(await this._chatSessionStore.readSession(sessionId));
|
||||
}
|
||||
@@ -558,11 +533,6 @@ export class ChatService extends Disposable implements IChatService {
|
||||
canUseTools: true,
|
||||
});
|
||||
|
||||
const isTransferred = this.transferredSessionData?.sessionId === sessionId;
|
||||
if (isTransferred) {
|
||||
this._transferredSessionData = undefined;
|
||||
}
|
||||
|
||||
return sessionRef;
|
||||
}
|
||||
|
||||
@@ -1309,22 +1279,25 @@ export class ChatService extends Disposable implements IChatService {
|
||||
return this._chatSessionStore.hasSessions();
|
||||
}
|
||||
|
||||
transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void {
|
||||
const model = Iterable.find(this._sessionModels.values(), model => model.sessionId === transferredSessionData.sessionId);
|
||||
if (!model) {
|
||||
throw new Error(`Failed to transfer session. Unknown session ID: ${transferredSessionData.sessionId}`);
|
||||
async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise<void> {
|
||||
if (!LocalChatSessionUri.isLocalSession(transferredSessionResource)) {
|
||||
throw new Error(`Can only transfer local chat sessions. Invalid session: ${transferredSessionResource}`);
|
||||
}
|
||||
|
||||
const existingRaw: IChatTransfer2[] = this.storageService.getObject(TransferredGlobalChatKey, StorageScope.PROFILE, []);
|
||||
existingRaw.push({
|
||||
chat: model.toJSON(),
|
||||
const model = this._sessionModels.get(transferredSessionResource) as ChatModel | undefined;
|
||||
if (!model) {
|
||||
throw new Error(`Failed to transfer session. Unknown session: ${transferredSessionResource}`);
|
||||
}
|
||||
|
||||
if (model.initialLocation !== ChatAgentLocation.Chat) {
|
||||
throw new Error(`Can only transfer chat sessions located in the Chat view. Session ${transferredSessionResource} has location=${model.initialLocation}`);
|
||||
}
|
||||
|
||||
await this._chatSessionStore.storeTransferSession({
|
||||
sessionResource: model.sessionResource,
|
||||
timestampInMilliseconds: Date.now(),
|
||||
toWorkspace: toWorkspace,
|
||||
inputState: transferredSessionData.inputState,
|
||||
location: transferredSessionData.location,
|
||||
});
|
||||
|
||||
this.storageService.store(TransferredGlobalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
}, model);
|
||||
this.chatTransferService.addWorkspaceToTransferred(toWorkspace);
|
||||
this.trace('transferChatSession', `Transferred session ${model.sessionResource} to workspace ${toWorkspace.toString()}`);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { Dto } from '../../../services/extensions/common/proxyIdentifier.js';
|
||||
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
|
||||
import { awaitStatsForSession } from './chat.js';
|
||||
import { ModifiedFileEntryState } from './chatEditingService.js';
|
||||
import { ChatModel, IChatModelInputState, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData } from './chatModel.js';
|
||||
import { ChatModel, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData } from './chatModel.js';
|
||||
import { IChatSessionStats, IChatSessionTiming, ResponseModelState } from './chatService.js';
|
||||
import { LocalChatSessionUri } from './chatUri.js';
|
||||
import { ChatAgentLocation } from './constants.js';
|
||||
@@ -30,12 +31,12 @@ import { ChatAgentLocation } from './constants.js';
|
||||
const maxPersistedSessions = 25;
|
||||
|
||||
const ChatIndexStorageKey = 'chat.ChatSessionStore.index';
|
||||
// const ChatTransferIndexStorageKey = 'ChatSessionStore.transferIndex';
|
||||
const ChatTransferIndexStorageKey = 'ChatSessionStore.transferIndex';
|
||||
|
||||
export class ChatSessionStore extends Disposable {
|
||||
private readonly storageRoot: URI;
|
||||
private readonly previousEmptyWindowStorageRoot: URI | undefined;
|
||||
// private readonly transferredSessionStorageRoot: URI;
|
||||
private readonly transferredSessionStorageRoot: URI;
|
||||
|
||||
private readonly storeQueue = new Sequencer();
|
||||
|
||||
@@ -65,8 +66,7 @@ export class ChatSessionStore extends Disposable {
|
||||
joinPath(this.environmentService.workspaceStorageHome, 'no-workspace', 'chatSessions') :
|
||||
undefined;
|
||||
|
||||
// TODO tmpdir
|
||||
// this.transferredSessionStorageRoot = joinPath(this.environmentService.workspaceStorageHome, 'transferredChatSessions');
|
||||
this.transferredSessionStorageRoot = joinPath(this.userDataProfilesService.defaultProfile.globalStorageHome, 'transferredChatSessions');
|
||||
|
||||
this._register(this.lifecycleService.onWillShutdown(e => {
|
||||
this.shuttingDown = true;
|
||||
@@ -124,33 +124,124 @@ export class ChatSessionStore extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
// async storeTransferSession(transferData: IChatTransfer, session: ISerializableChatData): Promise<void> {
|
||||
// try {
|
||||
// const content = JSON.stringify(session, undefined, 2);
|
||||
// await this.fileService.writeFile(this.transferredSessionStorageRoot, VSBuffer.fromString(content));
|
||||
// } catch (e) {
|
||||
// this.reportError('sessionWrite', 'Error writing chat session', e);
|
||||
// return;
|
||||
// }
|
||||
async storeTransferSession(transferData: IChatTransfer, session: ChatModel): Promise<void> {
|
||||
const index = this.getTransferredSessionIndex();
|
||||
const workspaceKey = transferData.toWorkspace.toString();
|
||||
|
||||
// const index = this.getTransferredSessionIndex();
|
||||
// index[transferData.toWorkspace.toString()] = transferData;
|
||||
// try {
|
||||
// this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
// } catch (e) {
|
||||
// this.reportError('storeTransferSession', 'Error storing chat transfer session', e);
|
||||
// }
|
||||
// }
|
||||
// Clean up any preexisting transferred session for this workspace
|
||||
const existingTransfer = index[workspaceKey];
|
||||
if (existingTransfer) {
|
||||
try {
|
||||
const existingSessionResource = URI.revive(existingTransfer.sessionResource);
|
||||
if (existingSessionResource && LocalChatSessionUri.parseLocalSessionId(existingSessionResource)) {
|
||||
const existingStorageLocation = this.getTransferredSessionStorageLocation(existingSessionResource);
|
||||
await this.fileService.del(existingStorageLocation);
|
||||
}
|
||||
} catch (e) {
|
||||
if (toFileOperationResult(e) !== FileOperationResult.FILE_NOT_FOUND) {
|
||||
this.reportError('storeTransferSession', 'Error deleting old transferred session file', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// private getTransferredSessionIndex(): IChatTransferIndex {
|
||||
// try {
|
||||
// const data: IChatTransferIndex = this.storageService.getObject(ChatTransferIndexStorageKey, StorageScope.PROFILE, {});
|
||||
// return data;
|
||||
// } catch (e) {
|
||||
// this.reportError('getTransferredSessionIndex', 'Error reading chat transfer index', e);
|
||||
// return {};
|
||||
// }
|
||||
// }
|
||||
try {
|
||||
const content = JSON.stringify(session, undefined, 2);
|
||||
const storageLocation = this.getTransferredSessionStorageLocation(session.sessionResource);
|
||||
await this.fileService.writeFile(storageLocation, VSBuffer.fromString(content));
|
||||
} catch (e) {
|
||||
this.reportError('sessionWrite', 'Error writing chat session', e);
|
||||
return;
|
||||
}
|
||||
|
||||
index[workspaceKey] = transferData;
|
||||
try {
|
||||
this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
} catch (e) {
|
||||
this.reportError('storeTransferSession', 'Error storing chat transfer session', e);
|
||||
}
|
||||
}
|
||||
|
||||
private getTransferredSessionIndex(): IChatTransferIndex {
|
||||
try {
|
||||
const data: IChatTransferIndex = this.storageService.getObject(ChatTransferIndexStorageKey, StorageScope.PROFILE, {});
|
||||
return data;
|
||||
} catch (e) {
|
||||
this.reportError('getTransferredSessionIndex', 'Error reading chat transfer index', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly TRANSFER_EXPIRATION_MS = 60 * 1000 * 5;
|
||||
|
||||
getTransferredSessionData(): URI | undefined {
|
||||
try {
|
||||
const index = this.getTransferredSessionIndex();
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
|
||||
if (workspaceFolders.length !== 1) {
|
||||
// Can only transfer sessions to single-folder workspaces
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const workspaceKey = workspaceFolders[0].uri.toString();
|
||||
const transferredSessionForWorkspace: IChatTransferDto = index[workspaceKey];
|
||||
if (!transferredSessionForWorkspace) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if the transfer has expired
|
||||
const revivedTransferData = revive(transferredSessionForWorkspace);
|
||||
if (Date.now() - transferredSessionForWorkspace.timestampInMilliseconds > ChatSessionStore.TRANSFER_EXPIRATION_MS) {
|
||||
this.logService.info('ChatSessionStore: Transferred session has expired');
|
||||
this.cleanupTransferredSession(revivedTransferData.sessionResource);
|
||||
return undefined;
|
||||
}
|
||||
return !!LocalChatSessionUri.parseLocalSessionId(revivedTransferData.sessionResource) && revivedTransferData.sessionResource;
|
||||
} catch (e) {
|
||||
this.reportError('getTransferredSession', 'Error getting transferred chat session URI', e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async readTransferredSession(sessionResource: URI): Promise<ISerializableChatData | undefined> {
|
||||
try {
|
||||
const storageLocation = this.getTransferredSessionStorageLocation(sessionResource);
|
||||
const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
|
||||
if (!sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sessionData = await this.readSessionFromLocation(storageLocation, sessionId);
|
||||
|
||||
// Clean up the transferred session after reading
|
||||
await this.cleanupTransferredSession(sessionResource);
|
||||
|
||||
return sessionData;
|
||||
} catch (e) {
|
||||
this.reportError('getTransferredSession', 'Error getting transferred chat session', e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupTransferredSession(sessionResource: URI): Promise<void> {
|
||||
try {
|
||||
// Remove from index
|
||||
const index = this.getTransferredSessionIndex();
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
|
||||
if (workspaceFolders.length === 1) {
|
||||
const workspaceKey = workspaceFolders[0].uri.toString();
|
||||
delete index[workspaceKey];
|
||||
this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
}
|
||||
|
||||
// Delete the transferred session file
|
||||
const storageLocation = this.getTransferredSessionStorageLocation(sessionResource);
|
||||
await this.fileService.del(storageLocation);
|
||||
} catch (e) {
|
||||
if (toFileOperationResult(e) !== FileOperationResult.FILE_NOT_FOUND) {
|
||||
this.reportError('cleanupTransferredSession', 'Error cleaning up transferred session', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async writeSession(session: ChatModel | ISerializableChatData): Promise<void> {
|
||||
try {
|
||||
@@ -359,45 +450,49 @@ export class ChatSessionStore extends Disposable {
|
||||
|
||||
public async readSession(sessionId: string): Promise<ISerializableChatData | undefined> {
|
||||
return await this.storeQueue.queue(async () => {
|
||||
let rawData: string | undefined;
|
||||
const storageLocation = this.getStorageLocation(sessionId);
|
||||
try {
|
||||
rawData = (await this.fileService.readFile(storageLocation)).value.toString();
|
||||
} catch (e) {
|
||||
this.reportError('sessionReadFile', `Error reading chat session file ${sessionId}`, e);
|
||||
return this.readSessionFromLocation(storageLocation, sessionId);
|
||||
});
|
||||
}
|
||||
|
||||
if (toFileOperationResult(e) === FileOperationResult.FILE_NOT_FOUND && this.previousEmptyWindowStorageRoot) {
|
||||
rawData = await this.readSessionFromPreviousLocation(sessionId);
|
||||
}
|
||||
private async readSessionFromLocation(storageLocation: URI, sessionId: string): Promise<ISerializableChatData | undefined> {
|
||||
let rawData: string | undefined;
|
||||
try {
|
||||
rawData = (await this.fileService.readFile(storageLocation)).value.toString();
|
||||
} catch (e) {
|
||||
this.reportError('sessionReadFile', `Error reading chat session file ${sessionId}`, e);
|
||||
|
||||
if (!rawData) {
|
||||
return undefined;
|
||||
}
|
||||
if (toFileOperationResult(e) === FileOperationResult.FILE_NOT_FOUND && this.previousEmptyWindowStorageRoot) {
|
||||
rawData = await this.readSessionFromPreviousLocation(sessionId);
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO Copied from ChatService.ts, cleanup
|
||||
const session: ISerializableChatDataIn = revive(JSON.parse(rawData)); // Revive serialized URIs in session data
|
||||
// Revive serialized markdown strings in response data
|
||||
for (const request of session.requests) {
|
||||
if (Array.isArray(request.response)) {
|
||||
request.response = request.response.map((response) => {
|
||||
if (typeof response === 'string') {
|
||||
return new MarkdownString(response);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
} else if (typeof request.response === 'string') {
|
||||
request.response = [new MarkdownString(request.response)];
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeSerializableChatData(session);
|
||||
} catch (err) {
|
||||
this.reportError('malformedSession', `Malformed session data in ${storageLocation.fsPath}: [${rawData.substring(0, 20)}${rawData.length > 20 ? '...' : ''}]`, err);
|
||||
if (!rawData) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO Copied from ChatService.ts, cleanup
|
||||
const session: ISerializableChatDataIn = revive(JSON.parse(rawData)); // Revive serialized URIs in session data
|
||||
// Revive serialized markdown strings in response data
|
||||
for (const request of session.requests) {
|
||||
if (Array.isArray(request.response)) {
|
||||
request.response = request.response.map((response) => {
|
||||
if (typeof response === 'string') {
|
||||
return new MarkdownString(response);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
} else if (typeof request.response === 'string') {
|
||||
request.response = [new MarkdownString(request.response)];
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeSerializableChatData(session);
|
||||
} catch (err) {
|
||||
this.reportError('malformedSession', `Malformed session data in ${storageLocation.fsPath}: [${rawData.substring(0, 20)}${rawData.length > 20 ? '...' : ''}]`, err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async readSessionFromPreviousLocation(sessionId: string): Promise<string | undefined> {
|
||||
@@ -421,6 +516,11 @@ export class ChatSessionStore extends Disposable {
|
||||
return joinPath(this.storageRoot, `${chatSessionId}.json`);
|
||||
}
|
||||
|
||||
private getTransferredSessionStorageLocation(sessionResource: URI): URI {
|
||||
const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
|
||||
return joinPath(this.transferredSessionStorageRoot, `${sessionId}.json`);
|
||||
}
|
||||
|
||||
public getChatStorageFolder(): URI {
|
||||
return this.storageRoot;
|
||||
}
|
||||
@@ -525,18 +625,17 @@ async function getSessionMetadata(session: ChatModel | ISerializableChatData): P
|
||||
|
||||
export interface IChatTransfer {
|
||||
toWorkspace: URI;
|
||||
sessionResource: URI;
|
||||
timestampInMilliseconds: number;
|
||||
inputState: IChatModelInputState | undefined;
|
||||
location: ChatAgentLocation;
|
||||
}
|
||||
|
||||
export interface IChatTransfer2 extends IChatTransfer {
|
||||
chat: ISerializableChatData;
|
||||
}
|
||||
|
||||
// type IChatTransferDto = Dto<IChatTransfer>;
|
||||
type IChatTransferDto = Dto<IChatTransfer>;
|
||||
|
||||
/**
|
||||
* Map of destination workspace URI to chat transfer data
|
||||
*/
|
||||
// type IChatTransferIndex = Record<string, IChatTransferDto>;
|
||||
type IChatTransferIndex = Record<string, IChatTransferDto>;
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ChatTransferService implements IChatTransferService {
|
||||
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService
|
||||
) { }
|
||||
|
||||
deleteWorkspaceFromTransferredList(workspace: URI): void {
|
||||
private deleteWorkspaceFromTransferredList(workspace: URI): void {
|
||||
const transferredWorkspaces = this.storageService.getObject<string[]>(transferredWorkspacesKey, StorageScope.PROFILE, []);
|
||||
const updatedWorkspaces = transferredWorkspaces.filter(uri => uri !== workspace.toString());
|
||||
this.storageService.store(transferredWorkspacesKey, updatedWorkspaces, StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
@@ -54,7 +54,7 @@ export class ChatTransferService implements IChatTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
isChatTransferredWorkspace(workspace: URI, storageService: IStorageService): boolean {
|
||||
private isChatTransferredWorkspace(workspace: URI, storageService: IStorageService): boolean {
|
||||
if (!workspace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ export namespace LocalChatSessionUri {
|
||||
return parsed?.chatSessionType === localChatSessionType ? parsed.sessionId : undefined;
|
||||
}
|
||||
|
||||
export function isLocalSession(resource: URI): boolean {
|
||||
return !!parseLocalSessionId(resource);
|
||||
}
|
||||
|
||||
function parse(resource: URI): ChatSessionIdentifier | undefined {
|
||||
if (resource.scheme !== scheme) {
|
||||
return undefined;
|
||||
|
||||
@@ -30,7 +30,7 @@ class MockChatService implements IChatService {
|
||||
edits2Enabled: boolean = false;
|
||||
_serviceBrand: undefined;
|
||||
editingSessions = [];
|
||||
transferredSessionData = undefined;
|
||||
transferredSessionResource = undefined;
|
||||
readonly onDidSubmitRequest = Event.None;
|
||||
|
||||
private sessions = new Map<string, IChatModel>();
|
||||
@@ -144,7 +144,7 @@ class MockChatService implements IChatService {
|
||||
|
||||
notifyUserAction(_event: any): void { }
|
||||
|
||||
transferChatSession(): void { }
|
||||
async transferChatSession(): Promise<void> { }
|
||||
|
||||
setChatSessionTitle(): void { }
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ILogService, NullLogService } from '../../../../../platform/log/common/
|
||||
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';
|
||||
import { IUserDataProfilesService, toUserDataProfile } from '../../../../../platform/userDataProfile/common/userDataProfile.js';
|
||||
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
|
||||
import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';
|
||||
import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js';
|
||||
@@ -158,6 +159,7 @@ suite('ChatService', () => {
|
||||
)));
|
||||
instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService()));
|
||||
instantiationService.stub(ILogService, new NullLogService());
|
||||
instantiationService.stub(IUserDataProfilesService, { defaultProfile: toUserDataProfile('default', 'Default', URI.file('/test/userdata'), URI.file('/test/cache')) });
|
||||
instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
instantiationService.stub(IExtensionService, new TestExtensionService());
|
||||
instantiationService.stub(IContextKeyService, new MockContextKeyService());
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
|
||||
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
|
||||
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';
|
||||
import { IUserDataProfilesService, toUserDataProfile } from '../../../../../platform/userDataProfile/common/userDataProfile.js';
|
||||
import { IWorkspaceContextService, WorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js';
|
||||
import { TestWorkspace, Workspace } from '../../../../../platform/workspace/test/common/testWorkspace.js';
|
||||
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
|
||||
import { InMemoryTestFileService, TestContextService, TestLifecycleService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
|
||||
import { ChatModel } from '../../common/chatModel.js';
|
||||
import { ChatSessionStore, IChatTransfer } from '../../common/chatSessionStore.js';
|
||||
import { LocalChatSessionUri } from '../../common/chatUri.js';
|
||||
import { MockChatModel } from './mockChatModel.js';
|
||||
|
||||
function createMockChatModel(sessionResource: URI, options?: { customTitle?: string }): ChatModel {
|
||||
const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
|
||||
if (!sessionId) {
|
||||
throw new Error('createMockChatModel requires a local session URI');
|
||||
}
|
||||
const model = new MockChatModel(sessionResource);
|
||||
model.sessionId = sessionId;
|
||||
if (options?.customTitle) {
|
||||
model.customTitle = options.customTitle;
|
||||
}
|
||||
// Cast to ChatModel - the mock implements enough of the interface for testing
|
||||
return model as unknown as ChatModel;
|
||||
}
|
||||
|
||||
suite('ChatSessionStore', () => {
|
||||
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
function createChatSessionStore(isEmptyWindow: boolean = false): ChatSessionStore {
|
||||
const workspace = isEmptyWindow ? new Workspace('empty-window-id', []) : TestWorkspace;
|
||||
instantiationService.stub(IWorkspaceContextService, new TestContextService(workspace));
|
||||
return testDisposables.add(instantiationService.createInstance(ChatSessionStore));
|
||||
}
|
||||
|
||||
setup(() => {
|
||||
instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection()));
|
||||
instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService()));
|
||||
instantiationService.stub(ILogService, NullLogService);
|
||||
instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
instantiationService.stub(IFileService, testDisposables.add(new InMemoryTestFileService()));
|
||||
instantiationService.stub(IEnvironmentService, { workspaceStorageHome: URI.file('/test/workspaceStorage') });
|
||||
instantiationService.stub(ILifecycleService, testDisposables.add(new TestLifecycleService()));
|
||||
instantiationService.stub(IUserDataProfilesService, { defaultProfile: toUserDataProfile('default', 'Default', URI.file('/test/userdata'), URI.file('/test/cache')) });
|
||||
});
|
||||
|
||||
test('hasSessions returns false when no sessions exist', () => {
|
||||
const store = createChatSessionStore();
|
||||
|
||||
assert.strictEqual(store.hasSessions(), false);
|
||||
});
|
||||
|
||||
test('getIndex returns empty index initially', async () => {
|
||||
const store = createChatSessionStore();
|
||||
|
||||
const index = await store.getIndex();
|
||||
assert.deepStrictEqual(index, {});
|
||||
});
|
||||
|
||||
test('getChatStorageFolder returns correct path for workspace', () => {
|
||||
const store = createChatSessionStore(false);
|
||||
|
||||
const storageFolder = store.getChatStorageFolder();
|
||||
assert.ok(storageFolder.path.includes('workspaceStorage'));
|
||||
assert.ok(storageFolder.path.includes('chatSessions'));
|
||||
});
|
||||
|
||||
test('getChatStorageFolder returns correct path for empty window', () => {
|
||||
const store = createChatSessionStore(true);
|
||||
|
||||
const storageFolder = store.getChatStorageFolder();
|
||||
assert.ok(storageFolder.path.includes('emptyWindowChatSessions'));
|
||||
});
|
||||
|
||||
test('isSessionEmpty returns true for non-existent session', () => {
|
||||
const store = createChatSessionStore();
|
||||
|
||||
assert.strictEqual(store.isSessionEmpty('non-existent-session'), true);
|
||||
});
|
||||
|
||||
test('readSession returns undefined for non-existent session', async () => {
|
||||
const store = createChatSessionStore();
|
||||
|
||||
const session = await store.readSession('non-existent-session');
|
||||
assert.strictEqual(session, undefined);
|
||||
});
|
||||
|
||||
test('deleteSession handles non-existent session gracefully', async () => {
|
||||
const store = createChatSessionStore();
|
||||
|
||||
// Should not throw
|
||||
await store.deleteSession('non-existent-session');
|
||||
|
||||
assert.strictEqual(store.hasSessions(), false);
|
||||
});
|
||||
|
||||
test('storeSessions persists session to index', async () => {
|
||||
const store = createChatSessionStore();
|
||||
const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1')));
|
||||
|
||||
await store.storeSessions([model]);
|
||||
|
||||
assert.strictEqual(store.hasSessions(), true);
|
||||
const index = await store.getIndex();
|
||||
assert.ok(index['session-1']);
|
||||
assert.strictEqual(index['session-1'].sessionId, 'session-1');
|
||||
});
|
||||
|
||||
test('storeSessions persists custom title', async () => {
|
||||
const store = createChatSessionStore();
|
||||
const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'), { customTitle: 'My Custom Title' }));
|
||||
|
||||
await store.storeSessions([model]);
|
||||
|
||||
const index = await store.getIndex();
|
||||
assert.strictEqual(index['session-1'].title, 'My Custom Title');
|
||||
});
|
||||
|
||||
test('readSession returns stored session data', async () => {
|
||||
const store = createChatSessionStore();
|
||||
const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1')));
|
||||
|
||||
await store.storeSessions([model]);
|
||||
const session = await store.readSession('session-1');
|
||||
|
||||
assert.ok(session);
|
||||
assert.strictEqual(session.sessionId, 'session-1');
|
||||
});
|
||||
|
||||
test('deleteSession removes session from index', async () => {
|
||||
const store = createChatSessionStore();
|
||||
const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1')));
|
||||
|
||||
await store.storeSessions([model]);
|
||||
assert.strictEqual(store.hasSessions(), true);
|
||||
|
||||
await store.deleteSession('session-1');
|
||||
|
||||
assert.strictEqual(store.hasSessions(), false);
|
||||
const index = await store.getIndex();
|
||||
assert.strictEqual(index['session-1'], undefined);
|
||||
});
|
||||
|
||||
test('clearAllSessions removes all sessions', async () => {
|
||||
const store = createChatSessionStore();
|
||||
const model1 = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1')));
|
||||
const model2 = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-2')));
|
||||
|
||||
await store.storeSessions([model1, model2]);
|
||||
assert.strictEqual(Object.keys(await store.getIndex()).length, 2);
|
||||
|
||||
await store.clearAllSessions();
|
||||
|
||||
const index = await store.getIndex();
|
||||
assert.deepStrictEqual(index, {});
|
||||
});
|
||||
|
||||
test('setSessionTitle updates existing session title', async () => {
|
||||
const store = createChatSessionStore();
|
||||
const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'), { customTitle: 'Original Title' }));
|
||||
|
||||
await store.storeSessions([model]);
|
||||
await store.setSessionTitle('session-1', 'New Title');
|
||||
|
||||
const index = await store.getIndex();
|
||||
assert.strictEqual(index['session-1'].title, 'New Title');
|
||||
});
|
||||
|
||||
test('setSessionTitle does nothing for non-existent session', async () => {
|
||||
const store = createChatSessionStore();
|
||||
|
||||
// Should not throw
|
||||
await store.setSessionTitle('non-existent', 'Title');
|
||||
|
||||
const index = await store.getIndex();
|
||||
assert.strictEqual(index['non-existent'], undefined);
|
||||
});
|
||||
|
||||
test('multiple stores can be created with different workspaces', async () => {
|
||||
const store1 = createChatSessionStore(false);
|
||||
const store2 = createChatSessionStore(true);
|
||||
|
||||
const folder1 = store1.getChatStorageFolder();
|
||||
const folder2 = store2.getChatStorageFolder();
|
||||
|
||||
assert.notStrictEqual(folder1.toString(), folder2.toString());
|
||||
});
|
||||
|
||||
suite('transferred sessions', () => {
|
||||
function createSingleFolderWorkspace(folderUri: URI): Workspace {
|
||||
const folder = new WorkspaceFolder({ uri: folderUri, index: 0, name: 'test' });
|
||||
return new Workspace('single-folder-id', [folder]);
|
||||
}
|
||||
|
||||
function createChatSessionStoreWithSingleFolder(folderUri: URI): ChatSessionStore {
|
||||
instantiationService.stub(IWorkspaceContextService, new TestContextService(createSingleFolderWorkspace(folderUri)));
|
||||
return testDisposables.add(instantiationService.createInstance(ChatSessionStore));
|
||||
}
|
||||
|
||||
function createTransferData(toWorkspace: URI, sessionResource: URI, timestampInMilliseconds?: number): IChatTransfer {
|
||||
return {
|
||||
toWorkspace,
|
||||
sessionResource,
|
||||
timestampInMilliseconds: timestampInMilliseconds ?? Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
test('getTransferredSessionData returns undefined for empty window', () => {
|
||||
const store = createChatSessionStore(true); // empty window
|
||||
|
||||
const result = store.getTransferredSessionData();
|
||||
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('getTransferredSessionData returns undefined when no transfer exists', () => {
|
||||
const folderUri = URI.file('/test/workspace');
|
||||
const store = createChatSessionStoreWithSingleFolder(folderUri);
|
||||
|
||||
const result = store.getTransferredSessionData();
|
||||
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('storeTransferSession stores and retrieves transfer data', async () => {
|
||||
const folderUri = URI.file('/test/workspace');
|
||||
const store = createChatSessionStoreWithSingleFolder(folderUri);
|
||||
const sessionResource = LocalChatSessionUri.forSession('transfer-session');
|
||||
const model = testDisposables.add(createMockChatModel(sessionResource));
|
||||
|
||||
const transferData = createTransferData(folderUri, sessionResource);
|
||||
await store.storeTransferSession(transferData, model);
|
||||
|
||||
const result = store.getTransferredSessionData();
|
||||
assert.ok(result);
|
||||
assert.strictEqual(result.toString(), sessionResource.toString());
|
||||
});
|
||||
|
||||
test('readTransferredSession returns session data', async () => {
|
||||
const folderUri = URI.file('/test/workspace');
|
||||
const store = createChatSessionStoreWithSingleFolder(folderUri);
|
||||
const sessionResource = LocalChatSessionUri.forSession('transfer-session');
|
||||
const model = testDisposables.add(createMockChatModel(sessionResource));
|
||||
|
||||
const transferData = createTransferData(folderUri, sessionResource);
|
||||
await store.storeTransferSession(transferData, model);
|
||||
|
||||
const sessionData = await store.readTransferredSession(sessionResource);
|
||||
assert.ok(sessionData);
|
||||
assert.strictEqual(sessionData.sessionId, 'transfer-session');
|
||||
});
|
||||
|
||||
test('readTransferredSession cleans up after reading', async () => {
|
||||
const folderUri = URI.file('/test/workspace');
|
||||
const store = createChatSessionStoreWithSingleFolder(folderUri);
|
||||
const sessionResource = LocalChatSessionUri.forSession('transfer-session');
|
||||
const model = testDisposables.add(createMockChatModel(sessionResource));
|
||||
|
||||
const transferData = createTransferData(folderUri, sessionResource);
|
||||
await store.storeTransferSession(transferData, model);
|
||||
|
||||
// Read the session
|
||||
await store.readTransferredSession(sessionResource);
|
||||
|
||||
// Transfer should be cleaned up
|
||||
const result = store.getTransferredSessionData();
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('getTransferredSessionData returns undefined for expired transfer', async () => {
|
||||
const folderUri = URI.file('/test/workspace');
|
||||
const store = createChatSessionStoreWithSingleFolder(folderUri);
|
||||
const sessionResource = LocalChatSessionUri.forSession('transfer-session');
|
||||
const model = testDisposables.add(createMockChatModel(sessionResource));
|
||||
|
||||
// Create transfer with timestamp 10 minutes in the past (expired)
|
||||
const expiredTimestamp = Date.now() - (10 * 60 * 1000);
|
||||
const transferData = createTransferData(folderUri, sessionResource, expiredTimestamp);
|
||||
await store.storeTransferSession(transferData, model);
|
||||
|
||||
const result = store.getTransferredSessionData();
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('expired transfer cleans up index and file', async () => {
|
||||
const folderUri = URI.file('/test/workspace');
|
||||
const store = createChatSessionStoreWithSingleFolder(folderUri);
|
||||
const sessionResource = LocalChatSessionUri.forSession('transfer-session');
|
||||
const model = testDisposables.add(createMockChatModel(sessionResource));
|
||||
|
||||
// Create transfer with timestamp 100 minutes in the past (expired)
|
||||
const expiredTimestamp = Date.now() - (100 * 60 * 1000);
|
||||
const transferData = createTransferData(folderUri, sessionResource, expiredTimestamp);
|
||||
await store.storeTransferSession(transferData, model);
|
||||
|
||||
// Assert cleaned up
|
||||
const data = store.getTransferredSessionData();
|
||||
assert.strictEqual(data, undefined);
|
||||
});
|
||||
|
||||
test('readTransferredSession returns undefined for invalid session resource', async () => {
|
||||
const folderUri = URI.file('/test/workspace');
|
||||
const store = createChatSessionStoreWithSingleFolder(folderUri);
|
||||
|
||||
// Use a non-local session URI
|
||||
const invalidResource = URI.parse('file:///invalid/session');
|
||||
|
||||
const result = await store.readTransferredSession(invalidResource);
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('storeTransferSession deletes preexisting transferred session file', async () => {
|
||||
const folderUri = URI.file('/test/workspace');
|
||||
const store = createChatSessionStoreWithSingleFolder(folderUri);
|
||||
const fileService = instantiationService.get(IFileService);
|
||||
|
||||
// Store first session
|
||||
const session1Resource = LocalChatSessionUri.forSession('transfer-session-1');
|
||||
const model1 = testDisposables.add(createMockChatModel(session1Resource));
|
||||
const transferData1 = createTransferData(folderUri, session1Resource);
|
||||
await store.storeTransferSession(transferData1, model1);
|
||||
|
||||
// Verify first session file exists
|
||||
const userDataProfile = instantiationService.get(IUserDataProfilesService).defaultProfile;
|
||||
const storageLocation1 = URI.joinPath(
|
||||
userDataProfile.globalStorageHome,
|
||||
'transferredChatSessions',
|
||||
'transfer-session-1.json'
|
||||
);
|
||||
const exists1 = await fileService.exists(storageLocation1);
|
||||
assert.strictEqual(exists1, true, 'First session file should exist');
|
||||
|
||||
// Store second session for the same workspace
|
||||
const session2Resource = LocalChatSessionUri.forSession('transfer-session-2');
|
||||
const model2 = testDisposables.add(createMockChatModel(session2Resource));
|
||||
const transferData2 = createTransferData(folderUri, session2Resource);
|
||||
await store.storeTransferSession(transferData2, model2);
|
||||
|
||||
// Verify first session file is deleted
|
||||
const exists1After = await fileService.exists(storageLocation1);
|
||||
assert.strictEqual(exists1After, false, 'First session file should be deleted');
|
||||
|
||||
// Verify second session file exists
|
||||
const storageLocation2 = URI.joinPath(
|
||||
userDataProfile.globalStorageHome,
|
||||
'transferredChatSessions',
|
||||
'transfer-session-2.json'
|
||||
);
|
||||
const exists2 = await fileService.exists(storageLocation2);
|
||||
assert.strictEqual(exists2, true, 'Second session file should exist');
|
||||
|
||||
// Verify only the second session is retrievable
|
||||
const result = store.getTransferredSessionData();
|
||||
assert.ok(result);
|
||||
assert.strictEqual(result.toString(), session2Resource.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,12 +14,16 @@ import { ChatAgentLocation } from '../../common/constants.js';
|
||||
export class MockChatModel extends Disposable implements IChatModel {
|
||||
readonly onDidDispose = this._register(new Emitter<void>()).event;
|
||||
readonly onDidChange = this._register(new Emitter<IChatChangeEvent>()).event;
|
||||
readonly sessionId = '';
|
||||
sessionId = '';
|
||||
readonly timestamp = 0;
|
||||
readonly timing = { startTime: 0 };
|
||||
readonly initialLocation = ChatAgentLocation.Chat;
|
||||
readonly title = '';
|
||||
readonly hasCustomTitle = false;
|
||||
customTitle: string | undefined;
|
||||
lastMessageDate = Date.now();
|
||||
creationDate = Date.now();
|
||||
requests: IChatRequestModel[] = [];
|
||||
readonly requestInProgress = observableValue('requestInProgress', false);
|
||||
readonly requestNeedsInput = observableValue<IChatRequestNeedsInputInfo | undefined>('requestNeedsInput', undefined);
|
||||
readonly inputPlaceholder = undefined;
|
||||
@@ -66,8 +70,8 @@ export class MockChatModel extends Disposable implements IChatModel {
|
||||
version: 3,
|
||||
sessionId: this.sessionId,
|
||||
creationDate: this.timestamp,
|
||||
lastMessageDate: this.timestamp,
|
||||
customTitle: undefined,
|
||||
lastMessageDate: this.lastMessageDate,
|
||||
customTitle: this.customTitle,
|
||||
initialLocation: this.initialLocation,
|
||||
requests: [],
|
||||
responderUsername: '',
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IObservable, observableValue } from '../../../../../base/common/observa
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../common/chatModel.js';
|
||||
import { IParsedChatRequest } from '../../common/chatParserTypes.js';
|
||||
import { IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatTransferredSessionData, IChatUserActionEvent } from '../../common/chatService.js';
|
||||
import { IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../common/chatService.js';
|
||||
import { ChatAgentLocation } from '../../common/constants.js';
|
||||
|
||||
export class MockChatService implements IChatService {
|
||||
@@ -19,7 +19,7 @@ export class MockChatService implements IChatService {
|
||||
edits2Enabled: boolean = false;
|
||||
_serviceBrand: undefined;
|
||||
editingSessions = [];
|
||||
transferredSessionData: IChatTransferredSessionData | undefined;
|
||||
transferredSessionResource: URI | undefined;
|
||||
readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }> = Event.None;
|
||||
|
||||
private sessions = new ResourceMap<IChatModel>();
|
||||
@@ -104,7 +104,7 @@ export class MockChatService implements IChatService {
|
||||
}
|
||||
readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = undefined!;
|
||||
|
||||
transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void {
|
||||
async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
|
||||
@@ -260,6 +260,22 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
|
||||
find: true,
|
||||
'/^find\\b.*-(delete|exec|execdir|fprint|fprintf|fls|ok|okdir)\\b/': false,
|
||||
|
||||
// sed
|
||||
// - `-e`/`--expression`: Add the commands in script to the set of commands to be run
|
||||
// while processing the input.
|
||||
// - `-f`/`--file`: Add the commands contained in the file script-file to the set of
|
||||
// commands to be run while processing the input.
|
||||
// - `-i`/`--in-place`: This option specifies that files are to be edited in-place.
|
||||
// - `w`/`W` commands: Write to files (blocked by `-i` check + agent typically won't use).
|
||||
// - `s///e` flag: Executes substitution result as shell command
|
||||
// - `s///w` flag: Write substitution result to file
|
||||
// - `;W` Write first line of pattern space to file
|
||||
// - Note that `--sandbox` exists which blocks unsafe commands that could potentially be
|
||||
// leveraged to auto approve
|
||||
sed: true,
|
||||
'/^sed\\b.*(-[a-zA-Z]*(e|i|f)[a-zA-Z]*|--expression|--file|--in-place)\\b/': false,
|
||||
'/^sed\\b.*(\/e|\/w|;W)/': false,
|
||||
|
||||
// sort
|
||||
// - `-o`: Output redirection can write files (`sort -o /etc/something file`) which are
|
||||
// blocked currently
|
||||
|
||||
@@ -243,6 +243,8 @@ suite('RunInTerminalTool', () => {
|
||||
'date +%Y-%m-%d',
|
||||
'find . -name "*.txt"',
|
||||
'grep pattern file.txt',
|
||||
'sed "s/foo/bar/g"',
|
||||
'sed -n "1,10p" file.txt',
|
||||
'sort file.txt',
|
||||
'tree directory'
|
||||
];
|
||||
@@ -295,6 +297,16 @@ suite('RunInTerminalTool', () => {
|
||||
'find . -exec rm {} \\;',
|
||||
'find . -execdir rm {} \\;',
|
||||
'find . -fprint output.txt',
|
||||
'sed -i "s/foo/bar/g" file.txt',
|
||||
'sed -i.bak "s/foo/bar/" file.txt',
|
||||
'sed --in-place "s/foo/bar/" file.txt',
|
||||
'sed -e "s/a/b/" file.txt',
|
||||
'sed -f script.sed file.txt',
|
||||
'sed --expression "s/a/b/" file.txt',
|
||||
'sed --file script.sed file.txt',
|
||||
'sed "s/foo/bar/e" file.txt',
|
||||
'sed "s/foo/bar/w output.txt" file.txt',
|
||||
'sed ";W output.txt" file.txt',
|
||||
'sort -o /etc/passwd file.txt',
|
||||
'sort -S 100G file.txt',
|
||||
'tree -o output.txt',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { $, Dimension, addDisposableListener, append, clearNode, reset } from '../../../../base/browser/dom.js';
|
||||
import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js';
|
||||
import { status } from '../../../../base/browser/ui/aria/aria.js';
|
||||
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
|
||||
import { Button } from '../../../../base/browser/ui/button/button.js';
|
||||
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
|
||||
@@ -295,6 +296,9 @@ export class GettingStartedPage extends EditorPane {
|
||||
badgeelement.setAttribute('aria-label', localize('stepNotDone', "Checkbox for Step {0}: Not completed", step.title));
|
||||
}
|
||||
});
|
||||
if (step.done) {
|
||||
status(localize('stepAutoCompleted', "Step {0} completed", step.title));
|
||||
}
|
||||
}
|
||||
this.updateCategoryProgress();
|
||||
}));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IContextMenuDelegate } from '../../../base/browser/contextmenu.js';
|
||||
import { IDimension } from '../../../base/browser/dom.js';
|
||||
import { Direction, IViewSize } from '../../../base/browser/ui/grid/grid.js';
|
||||
import { mainWindow } from '../../../base/browser/window.js';
|
||||
import { DeferredPromise, timeout } from '../../../base/common/async.js';
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from '../../../base/common/buffer.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../base/common/codicons.js';
|
||||
@@ -26,7 +26,6 @@ import { assertReturnsDefined, upcast } from '../../../base/common/types.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { ICodeEditor } from '../../../editor/browser/editorBrowser.js';
|
||||
import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';
|
||||
import { IMarkdownRendererService, MarkdownRendererService } from '../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { Position as EditorPosition, IPosition } from '../../../editor/common/core/position.js';
|
||||
import { Range } from '../../../editor/common/core/range.js';
|
||||
import { Selection } from '../../../editor/common/core/selection.js';
|
||||
@@ -83,6 +82,7 @@ import { ILabelService } from '../../../platform/label/common/label.js';
|
||||
import { ILayoutOffsetInfo } from '../../../platform/layout/browser/layoutService.js';
|
||||
import { IListService } from '../../../platform/list/browser/listService.js';
|
||||
import { ILoggerService, ILogService, NullLogService } from '../../../platform/log/common/log.js';
|
||||
import { IMarkdownRendererService, MarkdownRendererService } from '../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { IMarkerService } from '../../../platform/markers/common/markers.js';
|
||||
import { INotificationService } from '../../../platform/notification/common/notification.js';
|
||||
import { TestNotificationService } from '../../../platform/notification/test/common/testNotificationService.js';
|
||||
@@ -161,7 +161,7 @@ import { IHostService } from '../../services/host/browser/host.js';
|
||||
import { LabelService } from '../../services/label/common/labelService.js';
|
||||
import { ILanguageDetectionService } from '../../services/languageDetection/common/languageDetectionWorkerService.js';
|
||||
import { IWorkbenchLayoutService, PanelAlignment, Position as PartPosition, Parts } from '../../services/layout/browser/layoutService.js';
|
||||
import { BeforeShutdownErrorEvent, ILifecycleService, InternalBeforeShutdownEvent, IWillShutdownEventJoiner, LifecyclePhase, ShutdownReason, StartupKind, WillShutdownEvent } from '../../services/lifecycle/common/lifecycle.js';
|
||||
import { ILifecycleService, InternalBeforeShutdownEvent, IWillShutdownEventJoiner, ShutdownReason, WillShutdownEvent } from '../../services/lifecycle/common/lifecycle.js';
|
||||
import { IPaneCompositePartService } from '../../services/panecomposite/browser/panecomposite.js';
|
||||
import { IPathService } from '../../services/path/common/pathService.js';
|
||||
import { QuickInputService } from '../../services/quickinput/browser/quickInputService.js';
|
||||
@@ -185,10 +185,10 @@ import { InMemoryWorkingCopyBackupService } from '../../services/workingCopy/com
|
||||
import { IWorkingCopyEditorService, WorkingCopyEditorService } from '../../services/workingCopy/common/workingCopyEditorService.js';
|
||||
import { IWorkingCopyFileService, WorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js';
|
||||
import { IWorkingCopyService, WorkingCopyService } from '../../services/workingCopy/common/workingCopyService.js';
|
||||
import { TestChatEntitlementService, TestContextService, TestExtensionService, TestFileService, TestHistoryService, TestLoggerService, TestMarkerService, TestProductService, TestStorageService, TestTextResourcePropertiesService, TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from '../common/workbenchTestServices.js';
|
||||
import { TestChatEntitlementService, TestContextService, TestExtensionService, TestFileService, TestHistoryService, TestLifecycleService, TestLoggerService, TestMarkerService, TestProductService, TestStorageService, TestTextResourcePropertiesService, TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from '../common/workbenchTestServices.js';
|
||||
|
||||
// Backcompat export
|
||||
export { TestFileService };
|
||||
export { TestFileService, TestLifecycleService };
|
||||
|
||||
export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput {
|
||||
return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined);
|
||||
@@ -1187,88 +1187,6 @@ export class InMemoryTestWorkingCopyBackupService extends BrowserWorkingCopyBack
|
||||
}
|
||||
}
|
||||
|
||||
export class TestLifecycleService extends Disposable implements ILifecycleService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
usePhases = false;
|
||||
_phase!: LifecyclePhase;
|
||||
get phase(): LifecyclePhase { return this._phase; }
|
||||
set phase(value: LifecyclePhase) {
|
||||
this._phase = value;
|
||||
if (value === LifecyclePhase.Starting) {
|
||||
this.whenStarted.complete();
|
||||
} else if (value === LifecyclePhase.Ready) {
|
||||
this.whenReady.complete();
|
||||
} else if (value === LifecyclePhase.Restored) {
|
||||
this.whenRestored.complete();
|
||||
} else if (value === LifecyclePhase.Eventually) {
|
||||
this.whenEventually.complete();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly whenStarted = new DeferredPromise<void>();
|
||||
private readonly whenReady = new DeferredPromise<void>();
|
||||
private readonly whenRestored = new DeferredPromise<void>();
|
||||
private readonly whenEventually = new DeferredPromise<void>();
|
||||
async when(phase: LifecyclePhase): Promise<void> {
|
||||
if (!this.usePhases) {
|
||||
return;
|
||||
}
|
||||
if (phase === LifecyclePhase.Starting) {
|
||||
await this.whenStarted.p;
|
||||
} else if (phase === LifecyclePhase.Ready) {
|
||||
await this.whenReady.p;
|
||||
} else if (phase === LifecyclePhase.Restored) {
|
||||
await this.whenRestored.p;
|
||||
} else if (phase === LifecyclePhase.Eventually) {
|
||||
await this.whenEventually.p;
|
||||
}
|
||||
}
|
||||
|
||||
startupKind!: StartupKind;
|
||||
willShutdown = false;
|
||||
|
||||
private readonly _onBeforeShutdown = this._register(new Emitter<InternalBeforeShutdownEvent>());
|
||||
get onBeforeShutdown(): Event<InternalBeforeShutdownEvent> { return this._onBeforeShutdown.event; }
|
||||
|
||||
private readonly _onBeforeShutdownError = this._register(new Emitter<BeforeShutdownErrorEvent>());
|
||||
get onBeforeShutdownError(): Event<BeforeShutdownErrorEvent> { return this._onBeforeShutdownError.event; }
|
||||
|
||||
private readonly _onShutdownVeto = this._register(new Emitter<void>());
|
||||
get onShutdownVeto(): Event<void> { return this._onShutdownVeto.event; }
|
||||
|
||||
private readonly _onWillShutdown = this._register(new Emitter<WillShutdownEvent>());
|
||||
get onWillShutdown(): Event<WillShutdownEvent> { return this._onWillShutdown.event; }
|
||||
|
||||
private readonly _onDidShutdown = this._register(new Emitter<void>());
|
||||
get onDidShutdown(): Event<void> { return this._onDidShutdown.event; }
|
||||
|
||||
shutdownJoiners: Promise<void>[] = [];
|
||||
|
||||
fireShutdown(reason = ShutdownReason.QUIT): void {
|
||||
this.shutdownJoiners = [];
|
||||
|
||||
this._onWillShutdown.fire({
|
||||
join: p => {
|
||||
this.shutdownJoiners.push(typeof p === 'function' ? p() : p);
|
||||
},
|
||||
joiners: () => [],
|
||||
force: () => { /* No-Op in tests */ },
|
||||
token: CancellationToken.None,
|
||||
reason
|
||||
});
|
||||
}
|
||||
|
||||
fireBeforeShutdown(event: InternalBeforeShutdownEvent): void { this._onBeforeShutdown.fire(event); }
|
||||
|
||||
fireWillShutdown(event: WillShutdownEvent): void { this._onWillShutdown.fire(event); }
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
this.fireShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
export class TestBeforeShutdownEvent implements InternalBeforeShutdownEvent {
|
||||
|
||||
value: boolean | Promise<boolean> | undefined;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
import { DeferredPromise, timeout } from '../../../base/common/async.js';
|
||||
import { bufferToStream, readableToBuffer, VSBuffer, VSBufferReadable } from '../../../base/common/buffer.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
@@ -36,6 +36,7 @@ import { ChatEntitlement, IChatEntitlementService } from '../../services/chat/co
|
||||
import { NullExtensionService } from '../../services/extensions/common/extensions.js';
|
||||
import { IAutoSaveConfiguration, IAutoSaveMode, IFilesConfigurationService } from '../../services/filesConfiguration/common/filesConfigurationService.js';
|
||||
import { IHistoryService } from '../../services/history/common/history.js';
|
||||
import { BeforeShutdownErrorEvent, ILifecycleService, InternalBeforeShutdownEvent, LifecyclePhase, ShutdownReason, StartupKind, WillShutdownEvent } from '../../services/lifecycle/common/lifecycle.js';
|
||||
import { IResourceEncoding } from '../../services/textfile/common/textfiles.js';
|
||||
import { IUserDataProfileService } from '../../services/userDataProfile/common/userDataProfile.js';
|
||||
import { IStoredFileWorkingCopySaveEvent } from '../../services/workingCopy/common/storedFileWorkingCopy.js';
|
||||
@@ -698,7 +699,7 @@ export class TestFileService implements IFileService {
|
||||
*/
|
||||
export class InMemoryTestFileService extends TestFileService {
|
||||
|
||||
private files = new Map<string, VSBuffer>();
|
||||
private files = new ResourceMap<VSBuffer>();
|
||||
|
||||
override clearTracking(): void {
|
||||
super.clearTracking();
|
||||
@@ -714,7 +715,7 @@ export class InMemoryTestFileService extends TestFileService {
|
||||
this.readOperations.push({ resource });
|
||||
|
||||
// Check if we have content in our in-memory store
|
||||
const content = this.files.get(resource.toString());
|
||||
const content = this.files.get(resource);
|
||||
if (content) {
|
||||
return {
|
||||
...createFileStat(resource, this.readonly),
|
||||
@@ -743,11 +744,25 @@ export class InMemoryTestFileService extends TestFileService {
|
||||
}
|
||||
|
||||
// Store in memory and track
|
||||
this.files.set(resource.toString(), content);
|
||||
this.files.set(resource, content);
|
||||
this.writeOperations.push({ resource, content: content.toString() });
|
||||
|
||||
return createFileStat(resource, this.readonly);
|
||||
}
|
||||
|
||||
override async del(resource: URI, _options?: { useTrash?: boolean; recursive?: boolean }): Promise<void> {
|
||||
this.files.delete(resource);
|
||||
this.notExistsSet.set(resource, true);
|
||||
}
|
||||
|
||||
override async exists(resource: URI): Promise<boolean> {
|
||||
const inMemory = this.files.has(resource);
|
||||
if (inMemory) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.exists(resource);
|
||||
}
|
||||
}
|
||||
|
||||
export class TestChatEntitlementService implements IChatEntitlementService {
|
||||
@@ -779,3 +794,84 @@ export class TestChatEntitlementService implements IChatEntitlementService {
|
||||
readonly anonymousObs = observableValue({}, false);
|
||||
}
|
||||
|
||||
export class TestLifecycleService extends Disposable implements ILifecycleService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
usePhases = false;
|
||||
_phase!: LifecyclePhase;
|
||||
get phase(): LifecyclePhase { return this._phase; }
|
||||
set phase(value: LifecyclePhase) {
|
||||
this._phase = value;
|
||||
if (value === LifecyclePhase.Starting) {
|
||||
this.whenStarted.complete();
|
||||
} else if (value === LifecyclePhase.Ready) {
|
||||
this.whenReady.complete();
|
||||
} else if (value === LifecyclePhase.Restored) {
|
||||
this.whenRestored.complete();
|
||||
} else if (value === LifecyclePhase.Eventually) {
|
||||
this.whenEventually.complete();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly whenStarted = new DeferredPromise<void>();
|
||||
private readonly whenReady = new DeferredPromise<void>();
|
||||
private readonly whenRestored = new DeferredPromise<void>();
|
||||
private readonly whenEventually = new DeferredPromise<void>();
|
||||
async when(phase: LifecyclePhase): Promise<void> {
|
||||
if (!this.usePhases) {
|
||||
return;
|
||||
}
|
||||
if (phase === LifecyclePhase.Starting) {
|
||||
await this.whenStarted.p;
|
||||
} else if (phase === LifecyclePhase.Ready) {
|
||||
await this.whenReady.p;
|
||||
} else if (phase === LifecyclePhase.Restored) {
|
||||
await this.whenRestored.p;
|
||||
} else if (phase === LifecyclePhase.Eventually) {
|
||||
await this.whenEventually.p;
|
||||
}
|
||||
}
|
||||
|
||||
startupKind!: StartupKind;
|
||||
willShutdown = false;
|
||||
|
||||
private readonly _onBeforeShutdown = this._register(new Emitter<InternalBeforeShutdownEvent>());
|
||||
get onBeforeShutdown(): Event<InternalBeforeShutdownEvent> { return this._onBeforeShutdown.event; }
|
||||
|
||||
private readonly _onBeforeShutdownError = this._register(new Emitter<BeforeShutdownErrorEvent>());
|
||||
get onBeforeShutdownError(): Event<BeforeShutdownErrorEvent> { return this._onBeforeShutdownError.event; }
|
||||
|
||||
private readonly _onShutdownVeto = this._register(new Emitter<void>());
|
||||
get onShutdownVeto(): Event<void> { return this._onShutdownVeto.event; }
|
||||
|
||||
private readonly _onWillShutdown = this._register(new Emitter<WillShutdownEvent>());
|
||||
get onWillShutdown(): Event<WillShutdownEvent> { return this._onWillShutdown.event; }
|
||||
|
||||
private readonly _onDidShutdown = this._register(new Emitter<void>());
|
||||
get onDidShutdown(): Event<void> { return this._onDidShutdown.event; }
|
||||
|
||||
shutdownJoiners: Promise<void>[] = [];
|
||||
|
||||
fireShutdown(reason = ShutdownReason.QUIT): void {
|
||||
this.shutdownJoiners = [];
|
||||
|
||||
this._onWillShutdown.fire({
|
||||
join: p => {
|
||||
this.shutdownJoiners.push(typeof p === 'function' ? p() : p);
|
||||
},
|
||||
joiners: () => [],
|
||||
force: () => { /* No-Op in tests */ },
|
||||
token: CancellationToken.None,
|
||||
reason
|
||||
});
|
||||
}
|
||||
|
||||
fireBeforeShutdown(event: InternalBeforeShutdownEvent): void { this._onBeforeShutdown.fire(event); }
|
||||
|
||||
fireWillShutdown(event: WillShutdownEvent): void { this._onWillShutdown.fire(event); }
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
this.fireShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
declare module 'vscode' {
|
||||
|
||||
export namespace interactive {
|
||||
export function transferActiveChat(toWorkspace: Uri): void;
|
||||
export function transferActiveChat(toWorkspace: Uri): Thenable<void>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user