mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-27 22:21:24 +00:00
Merge branch 'main' into dev/dmitriv/fetch-tool-fixes
This commit is contained in:
commit
1663fb5c57
@ -274,18 +274,10 @@ export default tseslint.config(
|
||||
'src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts',
|
||||
'src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts',
|
||||
'src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts',
|
||||
'src/vs/workbench/contrib/chat/common/annotations.ts',
|
||||
'src/vs/workbench/contrib/chat/common/chat.ts',
|
||||
'src/vs/workbench/contrib/chat/common/chatAgents.ts',
|
||||
'src/vs/workbench/contrib/chat/common/chatModel.ts',
|
||||
'src/vs/workbench/contrib/chat/common/chatService.ts',
|
||||
'src/vs/workbench/contrib/chat/common/chatServiceImpl.ts',
|
||||
'src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts',
|
||||
'src/vs/workbench/contrib/chat/test/common/chatModel.test.ts',
|
||||
'src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts',
|
||||
'src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts',
|
||||
'src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts',
|
||||
'src/vs/workbench/contrib/debug/browser/breakpointsView.ts',
|
||||
'src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts',
|
||||
'src/vs/workbench/contrib/debug/browser/variablesView.ts',
|
||||
'src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts',
|
||||
|
||||
@ -1128,6 +1128,7 @@
|
||||
{
|
||||
"command": "git.repositories.openWorktreeInNewWindow",
|
||||
"title": "%command.openWorktreeInNewWindow2%",
|
||||
"icon": "$(folder-opened)",
|
||||
"category": "Git",
|
||||
"enablement": "!operationInProgress"
|
||||
},
|
||||
@ -2116,7 +2117,7 @@
|
||||
"when": "scmProvider == git && (scmArtifactGroupId == branches || scmArtifactGroupId == tags)"
|
||||
},
|
||||
{
|
||||
"command": "git.repositories.openWorktree",
|
||||
"command": "git.repositories.openWorktreeInNewWindow",
|
||||
"group": "inline@1",
|
||||
"when": "scmProvider == git && scmArtifactGroupId == worktrees"
|
||||
},
|
||||
|
||||
1613
extensions/terminal-suggest/src/completions/npm.ts
Normal file
1613
extensions/terminal-suggest/src/completions/npm.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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
@ -113,10 +113,7 @@ export const upstreamSpecs = [
|
||||
|
||||
// JavaScript / TypeScript
|
||||
'node',
|
||||
'npm',
|
||||
'nvm',
|
||||
'pnpm',
|
||||
'yarn',
|
||||
'yo',
|
||||
|
||||
// Python
|
||||
|
||||
@ -16,8 +16,11 @@ import codeTunnelInsidersCompletionSpec from './completions/code-tunnel-insiders
|
||||
import copilotSpec from './completions/copilot';
|
||||
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';
|
||||
@ -69,8 +72,11 @@ export const availableSpecs: Fig.Spec[] = [
|
||||
copilotSpec,
|
||||
gitCompletionSpec,
|
||||
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> {
|
||||
|
||||
@ -578,11 +578,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
|
||||
|
||||
this._sessionTypeToHandle.set(chatSessionScheme, handle);
|
||||
this._contentProvidersRegistrations.set(handle, this._chatSessionsService.registerChatSessionContentProvider(chatSessionScheme, provider));
|
||||
this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => {
|
||||
if (options?.optionGroups && options.optionGroups.length) {
|
||||
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, options.optionGroups);
|
||||
}
|
||||
}).catch(err => this._logService.error('Error fetching chat session options', err));
|
||||
this._refreshProviderOptions(handle, chatSessionScheme);
|
||||
}
|
||||
|
||||
$unregisterChatSessionContentProvider(handle: number): void {
|
||||
@ -634,6 +630,31 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
$onDidChangeChatSessionProviderOptions(handle: number): void {
|
||||
let sessionType: string | undefined;
|
||||
for (const [type, h] of this._sessionTypeToHandle) {
|
||||
if (h === handle) {
|
||||
sessionType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionType) {
|
||||
this._logService.warn(`No session type found for chat session content provider handle ${handle} when refreshing provider options`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._refreshProviderOptions(handle, sessionType);
|
||||
}
|
||||
|
||||
private _refreshProviderOptions(handle: number, chatSessionScheme: string): void {
|
||||
this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => {
|
||||
if (options?.optionGroups && options.optionGroups.length) {
|
||||
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, options.optionGroups);
|
||||
}
|
||||
}).catch(err => this._logService.error('Error fetching chat session options', err));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
for (const session of this._activeSessions.values()) {
|
||||
session.dispose();
|
||||
|
||||
@ -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 {
|
||||
@ -3305,6 +3305,7 @@ export interface MainThreadChatSessionsShape extends IDisposable {
|
||||
$registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void;
|
||||
$unregisterChatSessionContentProvider(handle: number): void;
|
||||
$onDidChangeChatSessionOptions(handle: number, sessionResource: UriComponents, updates: ReadonlyArray<ChatSessionOptionUpdateDto2>): void;
|
||||
$onDidChangeChatSessionProviderOptions(handle: number): void;
|
||||
|
||||
$handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise<void>;
|
||||
$handleAnchorResolve(handle: number, sessionResource: UriComponents, requestId: string, requestHandle: string, anchor: Dto<IChatContentInlineReference>): void;
|
||||
|
||||
@ -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 {
|
||||
@ -721,7 +721,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
|
||||
for (const h of context.history) {
|
||||
const ehResult = typeConvert.ChatAgentResult.to(h.result);
|
||||
const result: vscode.ChatResult = agentId === h.request.agentId ?
|
||||
const result: vscode.ChatResult = agentId === h.request.agentId || (isBuiltinParticipant(h.request.agentId) && isBuiltinParticipant(agentId)) ?
|
||||
ehResult :
|
||||
{ ...ehResult, metadata: undefined };
|
||||
|
||||
@ -1122,3 +1122,7 @@ function raceCancellationWithTimeout<T>(cancelWait: number, promise: Promise<T>,
|
||||
promise.then(resolve, reject).finally(() => ref.dispose());
|
||||
});
|
||||
}
|
||||
|
||||
function isBuiltinParticipant(agentId: string): boolean {
|
||||
return agentId.startsWith('github.copilot');
|
||||
}
|
||||
|
||||
@ -151,6 +151,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
|
||||
}));
|
||||
}
|
||||
|
||||
if (provider.onDidChangeChatSessionProviderOptions) {
|
||||
disposables.add(provider.onDidChangeChatSessionProviderOptions(() => {
|
||||
this._proxy.$onDidChangeChatSessionProviderOptions(handle);
|
||||
}));
|
||||
}
|
||||
|
||||
return new extHostTypes.Disposable(() => {
|
||||
this._chatSessionContentProviders.delete(handle);
|
||||
disposables.dispose();
|
||||
|
||||
@ -1125,6 +1125,9 @@ export class ExtHostSCM implements ExtHostSCMShape {
|
||||
|
||||
$setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void> {
|
||||
this.logService.trace('ExtHostSCM#$setSelectedSourceControl', selectedSourceControlHandle);
|
||||
if (this._selectedSourceControlHandle === selectedSourceControlHandle) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
if (selectedSourceControlHandle !== undefined) {
|
||||
this._sourceControls.get(selectedSourceControlHandle)?.setSelectionState(true);
|
||||
|
||||
@ -19,7 +19,7 @@ import { ILogService, NullLogService } from '../../../../platform/log/common/log
|
||||
import { ChatSessionsService } from '../../../contrib/chat/browser/chatSessions.contribution.js';
|
||||
import { IChatAgentRequest } from '../../../contrib/chat/common/chatAgents.js';
|
||||
import { IChatProgress, IChatProgressMessage, IChatService } from '../../../contrib/chat/common/chatService.js';
|
||||
import { IChatSessionItem, IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js';
|
||||
import { IChatSessionItem, IChatSessionProviderOptionGroup, IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js';
|
||||
import { LocalChatSessionUri } from '../../../contrib/chat/common/chatUri.js';
|
||||
import { ChatAgentLocation } from '../../../contrib/chat/common/constants.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
@ -517,4 +517,43 @@ suite('MainThreadChatSessions', function () {
|
||||
|
||||
mainThread.$unregisterChatSessionContentProvider(1);
|
||||
});
|
||||
|
||||
test('$onDidChangeChatSessionProviderOptions refreshes option groups', async function () {
|
||||
const sessionScheme = 'test-session-type';
|
||||
const handle = 1;
|
||||
|
||||
const optionGroups1: IChatSessionProviderOptionGroup[] = [{
|
||||
id: 'models',
|
||||
name: 'Models',
|
||||
items: [{ id: 'modelA', name: 'Model A' }]
|
||||
}];
|
||||
const optionGroups2: IChatSessionProviderOptionGroup[] = [{
|
||||
id: 'models',
|
||||
name: 'Models',
|
||||
items: [{ id: 'modelB', name: 'Model B' }]
|
||||
}];
|
||||
|
||||
const provideOptionsStub = proxy.$provideChatSessionProviderOptions as sinon.SinonStub;
|
||||
provideOptionsStub.onFirstCall().resolves({ optionGroups: optionGroups1 } as IChatSessionProviderOptions);
|
||||
provideOptionsStub.onSecondCall().resolves({ optionGroups: optionGroups2 } as IChatSessionProviderOptions);
|
||||
|
||||
mainThread.$registerChatSessionContentProvider(handle, sessionScheme);
|
||||
|
||||
// Wait for initial options fetch triggered on registration
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
let storedGroups = chatSessionsService.getOptionGroupsForSessionType(sessionScheme);
|
||||
assert.ok(storedGroups);
|
||||
assert.strictEqual(storedGroups![0].items[0].id, 'modelA');
|
||||
|
||||
// Simulate extension signaling that provider options have changed
|
||||
mainThread.$onDidChangeChatSessionProviderOptions(handle);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
storedGroups = chatSessionsService.getOptionGroupsForSessionType(sessionScheme);
|
||||
assert.ok(storedGroups);
|
||||
assert.strictEqual(storedGroups![0].items[0].id, 'modelB');
|
||||
|
||||
mainThread.$unregisterChatSessionContentProvider(handle);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,6 +84,15 @@ export class ImplicitContextAttachmentWidget extends Disposable {
|
||||
}
|
||||
this.attachment.enabled = false;
|
||||
}));
|
||||
} else {
|
||||
const pinButtonMsg = localize('pinSelection', "Pin selection");
|
||||
const pinButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: pinButtonMsg }));
|
||||
pinButton.icon = Codicon.pinned;
|
||||
this.renderDisposables.add(pinButton.onDidClick(async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
await this.pinSelection();
|
||||
}));
|
||||
}
|
||||
|
||||
if (!this.attachment.enabled && this.attachment.isSelection) {
|
||||
@ -209,4 +218,15 @@ export class ImplicitContextAttachmentWidget extends Disposable {
|
||||
}
|
||||
this.widgetRef()?.focusInput();
|
||||
}
|
||||
private async pinSelection(): Promise<void> {
|
||||
if (!this.attachment.value || !this.attachment.isSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!URI.isUri(this.attachment.value) && !isStringImplicitContextValue(this.attachment.value)) {
|
||||
const location = this.attachment.value;
|
||||
this.attachmentModel.addFile(location.uri, location.range);
|
||||
}
|
||||
this.widgetRef()?.focusInput();
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,8 +96,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler
|
||||
|
||||
// Check if we already have a custom title for this session
|
||||
const hasExistingCustomTitle = this._sessionResource && (
|
||||
this.chatService.getSession(this._sessionResource)?.title ||
|
||||
this.chatService.getPersistedSessionTitle(this._sessionResource)?.trim()
|
||||
this.chatService.getSessionTitle(this._sessionResource)?.trim()
|
||||
);
|
||||
|
||||
this.hasCustomTitle = Boolean(hasExistingCustomTitle);
|
||||
@ -184,7 +183,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler
|
||||
}
|
||||
|
||||
// If not in active registry, try persisted session data
|
||||
const persistedTitle = this.chatService.getPersistedSessionTitle(this._sessionResource);
|
||||
const persistedTitle = this.chatService.getSessionTitle(this._sessionResource);
|
||||
if (persistedTitle && persistedTitle.trim()) { // Only use non-empty persisted titles
|
||||
return persistedTitle;
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/c
|
||||
import { EditorOptions, IEditorOptions } from '../../../../editor/common/config/editorOptions.js';
|
||||
import { IDimension } from '../../../../editor/common/core/2d/dimension.js';
|
||||
import { IPosition } from '../../../../editor/common/core/position.js';
|
||||
import { IRange, Range } from '../../../../editor/common/core/range.js';
|
||||
import { isLocation } from '../../../../editor/common/languages.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
@ -1475,7 +1476,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
|
||||
this.tryUpdateWidgetController();
|
||||
|
||||
this.renderAttachedContext();
|
||||
this._register(this._attachmentModel.onDidChange((e) => {
|
||||
if (e.added.length > 0) {
|
||||
this._indexOfLastAttachedContextDeletedWithKeyboard = -1;
|
||||
@ -1758,6 +1758,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this._onDidChangeHeight.fire();
|
||||
}
|
||||
}));
|
||||
this.renderAttachedContext();
|
||||
}
|
||||
|
||||
public toggleChatInputOverlay(editing: boolean): void {
|
||||
@ -1859,18 +1860,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
|
||||
if (isSuggestedEnabled && implicitValue) {
|
||||
const targetUri: URI | undefined = this.implicitContext.uri;
|
||||
|
||||
const currentlyAttached = attachments.some(([, attachment]) => {
|
||||
let uri: URI | undefined;
|
||||
if (URI.isUri(attachment.value)) {
|
||||
uri = attachment.value;
|
||||
} else if (isStringVariableEntry(attachment)) {
|
||||
uri = attachment.uri;
|
||||
}
|
||||
return uri && isEqual(uri, targetUri);
|
||||
});
|
||||
|
||||
const shouldShowImplicit = !isLocation(implicitValue) ? !currentlyAttached : implicitValue.range;
|
||||
const targetRange = isLocation(implicitValue) ? implicitValue.range : undefined;
|
||||
const currentlyAttached = this.isAttachmentAlreadyAttached(targetUri, targetRange, attachments.map(([, a]) => a));
|
||||
const shouldShowImplicit = !currentlyAttached;
|
||||
if (shouldShowImplicit) {
|
||||
const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, () => this._widget, this.implicitContext, this._contextResourceLabels, this._attachmentModel));
|
||||
container.appendChild(implicitPart.domNode);
|
||||
@ -1896,18 +1888,44 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO @justschen: merge this with above showing implicit logic
|
||||
const isUri = URI.isUri(implicit);
|
||||
if (isUri || isLocation(implicit)) {
|
||||
const targetUri = isUri ? implicit : implicit.uri;
|
||||
const attachments = [...this._attachmentModel.attachments.entries()];
|
||||
const currentlyAttached = attachments.some(([, a]) => URI.isUri(a.value) && isEqual(a.value, targetUri));
|
||||
const shouldShowImplicit = isUri ? !currentlyAttached : implicit.range;
|
||||
return !!shouldShowImplicit;
|
||||
const targetRange = isLocation(implicit) ? implicit.range : undefined;
|
||||
const attachments = [...this._attachmentModel.attachments.values()];
|
||||
const currentlyAttached = this.isAttachmentAlreadyAttached(targetUri, targetRange, attachments);
|
||||
return !currentlyAttached;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isAttachmentAlreadyAttached(targetUri: URI | undefined, targetRange: IRange | undefined, attachments: IChatRequestVariableEntry[]): boolean {
|
||||
return attachments.some((attachment) => {
|
||||
let uri: URI | undefined;
|
||||
let range: IRange | undefined;
|
||||
|
||||
if (URI.isUri(attachment.value)) {
|
||||
uri = attachment.value;
|
||||
} else if (isLocation(attachment.value)) {
|
||||
uri = attachment.value.uri;
|
||||
range = attachment.value.range;
|
||||
} else if (isStringVariableEntry(attachment)) {
|
||||
uri = attachment.uri;
|
||||
}
|
||||
|
||||
if (!uri || !isEqual(uri, targetUri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the exact range is already attached
|
||||
if (targetRange) {
|
||||
return range && Range.equalsRange(range, targetRange);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private handleAttachmentDeletion(e: KeyboardEvent | unknown, index: number, attachment: IChatRequestVariableEntry) {
|
||||
// Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click)
|
||||
if (dom.isKeyboardEvent(e)) {
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -1593,6 +1593,10 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.action-item.chat-attachment-button .action-label {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.interactive-session .interactive-list .chat-attached-context .chat-attached-context-attachment {
|
||||
font-family: var(--vscode-chat-font-family, inherit);
|
||||
font-size: var(--vscode-chat-font-size-body-xs);
|
||||
|
||||
@ -7,6 +7,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { basename } from '../../../../base/common/resources.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { isLocation } from '../../../../editor/common/languages.js';
|
||||
import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from './chatModel.js';
|
||||
import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from './chatService.js';
|
||||
|
||||
@ -24,10 +25,10 @@ export function annotateSpecialMarkdownContent(response: Iterable<IChatProgressR
|
||||
if (!label) {
|
||||
if (URI.isUri(item.inlineReference)) {
|
||||
label = basename(item.inlineReference);
|
||||
} else if ('name' in item.inlineReference) {
|
||||
label = item.inlineReference.name;
|
||||
} else {
|
||||
} else if (isLocation(item.inlineReference)) {
|
||||
label = basename(item.inlineReference.uri);
|
||||
} else {
|
||||
label = item.inlineReference.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { ResourceSet } from '../../../../base/common/map.js';
|
||||
import { chatEditingSessionIsReady } from './chatEditingService.js';
|
||||
import { IChatModel } from './chatModel.js';
|
||||
import type { IChatSessionStats, IChatTerminalToolInvocationData, ILegacyChatTerminalToolInvocationData } from './chatService.js';
|
||||
import { isLegacyChatTerminalToolInvocationData, type IChatSessionStats, type IChatTerminalToolInvocationData, type ILegacyChatTerminalToolInvocationData } from './chatService.js';
|
||||
import { ChatModeKind } from './constants.js';
|
||||
|
||||
export function checkModeOption(mode: ChatModeKind, option: boolean | ((mode: ChatModeKind) => boolean) | undefined): boolean | undefined {
|
||||
@ -24,7 +24,7 @@ export function checkModeOption(mode: ChatModeKind, option: boolean | ((mode: Ch
|
||||
* we don't break existing chats
|
||||
*/
|
||||
export function migrateLegacyTerminalToolSpecificData(data: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData): IChatTerminalToolInvocationData {
|
||||
if ('command' in data) {
|
||||
if (isLegacyChatTerminalToolInvocationData(data)) {
|
||||
data = {
|
||||
kind: 'terminal',
|
||||
commandLine: {
|
||||
|
||||
@ -743,8 +743,12 @@ interface IOldSerializedChatAgentData extends Omit<ISerializableChatAgentData, '
|
||||
extensionPublisher?: string;
|
||||
}
|
||||
|
||||
function isSerializableChatAgentData(obj: ISerializableChatAgentData | IOldSerializedChatAgentData): obj is ISerializableChatAgentData {
|
||||
return (obj as ISerializableChatAgentData).name !== undefined;
|
||||
}
|
||||
|
||||
export function reviveSerializedAgent(raw: ISerializableChatAgentData | IOldSerializedChatAgentData): IChatAgentData {
|
||||
const normalized: ISerializableChatAgentData = 'name' in raw ?
|
||||
const normalized: ISerializableChatAgentData = isSerializableChatAgentData(raw) ?
|
||||
raw :
|
||||
{
|
||||
...raw,
|
||||
|
||||
@ -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';
|
||||
@ -104,6 +104,12 @@ export interface IChatContentVariableReference {
|
||||
value?: URI | Location;
|
||||
}
|
||||
|
||||
export function isChatContentVariableReference(obj: unknown): obj is IChatContentVariableReference {
|
||||
return !!obj &&
|
||||
typeof obj === 'object' &&
|
||||
typeof (obj as IChatContentVariableReference).variableName === 'string';
|
||||
}
|
||||
|
||||
export enum ChatResponseReferencePartStatusKind {
|
||||
Complete = 1,
|
||||
Partial = 2,
|
||||
@ -402,6 +408,10 @@ export interface ILegacyChatTerminalToolInvocationData {
|
||||
language: string;
|
||||
}
|
||||
|
||||
export function isLegacyChatTerminalToolInvocationData(data: unknown): data is ILegacyChatTerminalToolInvocationData {
|
||||
return !!data && typeof data === 'object' && 'command' in data;
|
||||
}
|
||||
|
||||
export interface IChatToolInputInvocationData {
|
||||
kind: 'input';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -604,7 +614,7 @@ export namespace IChatToolInvocation {
|
||||
}
|
||||
|
||||
export function isComplete(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): boolean {
|
||||
if ('isComplete' in invocation) { // serialized
|
||||
if (invocation.kind === 'toolInvocationSerialized') {
|
||||
return true; // always cancelled or complete
|
||||
}
|
||||
|
||||
@ -934,12 +944,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 +1010,7 @@ export const IChatService = createDecorator<IChatService>('IChatService');
|
||||
|
||||
export interface IChatService {
|
||||
_serviceBrand: undefined;
|
||||
transferredSessionData: IChatTransferredSessionData | undefined;
|
||||
transferredSessionResource: URI | undefined;
|
||||
|
||||
readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }>;
|
||||
|
||||
@ -1030,8 +1034,7 @@ export interface IChatService {
|
||||
getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined;
|
||||
|
||||
getOrRestoreSession(sessionResource: URI): Promise<IChatModelReference | undefined>;
|
||||
getPersistedSessionTitle(sessionResource: URI): string | undefined;
|
||||
isPersistedSessionEmpty(sessionResource: URI): boolean;
|
||||
getSessionTitle(sessionResource: URI): string | undefined;
|
||||
loadSessionFromContent(data: IExportableChatData | ISerializableChatData | URI): IChatModelReference | undefined;
|
||||
loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModelReference | undefined>;
|
||||
readonly editingSessions: IChatEditingSession[];
|
||||
@ -1066,7 +1069,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,
|
||||
@ -79,12 +76,11 @@ export class ChatService extends Disposable implements IChatService {
|
||||
|
||||
private readonly _sessionModels: ChatModelStore;
|
||||
private readonly _pendingRequests = this._register(new DisposableResourceMap<CancellableRequest>());
|
||||
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 }>());
|
||||
@ -163,38 +159,16 @@ export class ChatService extends Disposable implements IChatService {
|
||||
}));
|
||||
|
||||
this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry);
|
||||
|
||||
const sessionData = storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, '');
|
||||
if (sessionData) {
|
||||
this._persistedSessions = this.deserializeChats(sessionData);
|
||||
const countsForLog = Object.keys(this._persistedSessions).length;
|
||||
if (countsForLog > 0) {
|
||||
this.trace('constructor', `Restored ${countsForLog} persisted sessions`);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
this._chatSessionStore.migrateDataIfNeeded(() => this.migrateData());
|
||||
|
||||
// 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(() => {
|
||||
this.reviveSessionsWithEdits();
|
||||
});
|
||||
const transferredData = this._chatSessionStore.getTransferredSessionData();
|
||||
if (transferredData) {
|
||||
this.trace('constructor', `Transferred session ${transferredData}`);
|
||||
this._transferredSessionResource = transferredData;
|
||||
}
|
||||
|
||||
this.reviveSessionsWithEdits();
|
||||
|
||||
this._register(storageService.onWillSaveState(() => this.saveState()));
|
||||
|
||||
@ -214,6 +188,21 @@ export class ChatService extends Disposable implements IChatService {
|
||||
return this.chatAgentService.getContributedDefaultAgent(location) !== undefined;
|
||||
}
|
||||
|
||||
private migrateData(): ISerializableChatsData | undefined {
|
||||
const sessionData = this.storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, '');
|
||||
if (sessionData) {
|
||||
const persistedSessions = this.deserializeChats(sessionData);
|
||||
const countsForLog = Object.keys(persistedSessions).length;
|
||||
if (countsForLog > 0) {
|
||||
this.info('migrateData', `Restored ${countsForLog} persisted sessions`);
|
||||
}
|
||||
|
||||
return persistedSessions;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private saveState(): void {
|
||||
if (!this._saveModelsEnabled) {
|
||||
return;
|
||||
@ -273,6 +262,14 @@ export class ChatService extends Disposable implements IChatService {
|
||||
}
|
||||
}
|
||||
|
||||
private info(method: string, message?: string): void {
|
||||
if (message) {
|
||||
this.logService.info(`ChatService#${method}: ${message}`);
|
||||
} else {
|
||||
this.logService.info(`ChatService#${method}`);
|
||||
}
|
||||
}
|
||||
|
||||
private error(method: string, message: string): void {
|
||||
this.logService.error(`ChatService#${method} ${message}`);
|
||||
}
|
||||
@ -309,28 +306,12 @@ 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.
|
||||
*/
|
||||
private async reviveSessionsWithEdits(): Promise<void> {
|
||||
await Promise.all(Object.values(this._persistedSessions).map(async session => {
|
||||
const idx = await this._chatSessionStore.getIndex();
|
||||
await Promise.all(Object.values(idx).map(async session => {
|
||||
if (!session.hasPendingEdits) {
|
||||
return;
|
||||
}
|
||||
@ -345,34 +326,6 @@ export class ChatService extends Disposable implements IChatService {
|
||||
}));
|
||||
}
|
||||
|
||||
private async initializePersistedSessionsFromFileStorage(): Promise<void> {
|
||||
|
||||
const index = await this._chatSessionStore.getIndex();
|
||||
const sessionIds = Object.keys(index);
|
||||
|
||||
for (const sessionId of sessionIds) {
|
||||
const metadata = index[sessionId];
|
||||
if (metadata && !this._persistedSessions[sessionId]) {
|
||||
// Create a minimal session entry with the title information
|
||||
// This allows getPersistedSessionTitle() to find the title without loading the full session
|
||||
const minimalSession: ISerializableChatData = {
|
||||
version: 3,
|
||||
sessionId: sessionId,
|
||||
customTitle: metadata.title,
|
||||
creationDate: Date.now(), // Use current time as fallback
|
||||
lastMessageDate: metadata.lastMessageDate,
|
||||
initialLocation: metadata.initialLocation,
|
||||
requests: [], // Empty requests array - this is just for title lookup
|
||||
responderUsername: '',
|
||||
responderAvatarIconUri: undefined,
|
||||
hasPendingEdits: metadata.hasPendingEdits,
|
||||
};
|
||||
|
||||
this._persistedSessions[sessionId] = minimalSession;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of chat details for all persisted chat sessions that have at least one request.
|
||||
* Chat sessions that have already been loaded into the chat view are excluded from the result.
|
||||
@ -540,8 +493,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,63 +512,23 @@ export class ChatService extends Disposable implements IChatService {
|
||||
canUseTools: true,
|
||||
});
|
||||
|
||||
const isTransferred = this.transferredSessionData?.sessionId === sessionId;
|
||||
if (isTransferred) {
|
||||
this._transferredSessionData = undefined;
|
||||
}
|
||||
|
||||
return sessionRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is really just for migrating data from the edit session location to the panel.
|
||||
*/
|
||||
isPersistedSessionEmpty(sessionResource: URI): boolean {
|
||||
const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
|
||||
if (!sessionId) {
|
||||
throw new Error(`Cannot restore non-local session ${sessionResource}`);
|
||||
}
|
||||
|
||||
const session = this._persistedSessions[sessionId];
|
||||
if (session) {
|
||||
return session.requests.length === 0;
|
||||
}
|
||||
|
||||
return this._chatSessionStore.isSessionEmpty(sessionId);
|
||||
}
|
||||
|
||||
getPersistedSessionTitle(sessionResource: URI): string | undefined {
|
||||
// There are some cases where this returns a real string. What happens if it doesn't?
|
||||
// This had titles restored from the index, so just return titles from index instead, sync.
|
||||
getSessionTitle(sessionResource: URI): string | undefined {
|
||||
const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
|
||||
if (!sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// First check the memory cache (_persistedSessions)
|
||||
const session = this._persistedSessions[sessionId];
|
||||
if (session) {
|
||||
const title = session.customTitle || ChatModel.getDefaultTitle(session.requests);
|
||||
return title;
|
||||
}
|
||||
|
||||
// Try to read directly from file storage index
|
||||
// This handles the case where getName() is called before initialization completes
|
||||
// Access the internal synchronous index method via reflection
|
||||
// This is a workaround for the timing issue where initialization hasn't completed
|
||||
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
|
||||
const internalGetIndex = (this._chatSessionStore as any).internalGetIndex;
|
||||
if (typeof internalGetIndex === 'function') {
|
||||
const indexData = internalGetIndex.call(this._chatSessionStore);
|
||||
const metadata = indexData.entries[sessionId];
|
||||
if (metadata && metadata.title) {
|
||||
return metadata.title;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return this._sessionModels.get(sessionResource)?.title ??
|
||||
this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.title;
|
||||
}
|
||||
|
||||
loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModelReference | undefined {
|
||||
const sessionId = 'sessionId' in data && data.sessionId ? data.sessionId : generateUuid();
|
||||
const sessionId = (data as ISerializableChatData).sessionId ?? generateUuid();
|
||||
const sessionResource = LocalChatSessionUri.forSession(sessionId);
|
||||
return this._sessionModels.acquireOrCreate({
|
||||
initialData: data,
|
||||
@ -874,9 +788,9 @@ export class ChatService extends Disposable implements IChatService {
|
||||
private _sendRequestAsync(model: ChatModel, sessionResource: URI, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgentData, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState {
|
||||
const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionResource);
|
||||
let request: ChatRequestModel;
|
||||
const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
|
||||
const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
|
||||
const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart);
|
||||
const agentPart = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
|
||||
const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
|
||||
const commandPart = parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart);
|
||||
const requests = [...model.getRequests()];
|
||||
const requestTelemetry = this.instantiationService.createInstance(ChatRequestTelemetry, {
|
||||
agent: agentPart?.agent ?? defaultAgent,
|
||||
@ -1309,22 +1223,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 {
|
||||
@ -328,6 +419,16 @@ export class ChatSessionStore extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
getMetadataForSessionSync(sessionResource: URI): IChatSessionEntryMetadata | undefined {
|
||||
const index = this.internalGetIndex();
|
||||
return index.entries[this.getIndexKey(sessionResource)];
|
||||
}
|
||||
|
||||
private getIndexKey(sessionResource: URI): string {
|
||||
const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
|
||||
return sessionId ?? sessionResource.toString();
|
||||
}
|
||||
|
||||
logIndex(): void {
|
||||
const data = this.storageService.get(ChatIndexStorageKey, this.getIndexStorageScope(), undefined);
|
||||
this.logService.info('ChatSessionStore index: ', data);
|
||||
@ -359,45 +460,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 +526,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 +635,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;
|
||||
|
||||
@ -14,6 +14,7 @@ import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modes
|
||||
import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';
|
||||
import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js';
|
||||
import { isChatContentVariableReference } from './chatService.js';
|
||||
import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from './chatViewModel.js';
|
||||
|
||||
|
||||
@ -240,7 +241,7 @@ export class CodeBlockModelCollection extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
const uriOrLocation = 'variableName' in ref.reference ?
|
||||
const uriOrLocation = isChatContentVariableReference(ref.reference) ?
|
||||
ref.reference.value :
|
||||
ref.reference;
|
||||
if (!uriOrLocation) {
|
||||
|
||||
@ -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>();
|
||||
@ -92,7 +92,7 @@ class MockChatService implements IChatService {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getPersistedSessionTitle(_sessionResource: URI): string | undefined {
|
||||
getSessionTitle(_sessionResource: URI): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ class MockChatService implements IChatService {
|
||||
|
||||
notifyUserAction(_event: any): void { }
|
||||
|
||||
transferChatSession(): void { }
|
||||
async transferChatSession(): Promise<void> { }
|
||||
|
||||
setChatSessionTitle(): void { }
|
||||
|
||||
@ -158,10 +158,6 @@ class MockChatService implements IChatService {
|
||||
|
||||
logChatIndex(): void { }
|
||||
|
||||
isPersistedSessionEmpty(_sessionResource: URI): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
activateDefaultAgent(_location: ChatAgentLocation): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -263,7 +263,6 @@ suite('normalizeSerializableChatData', () => {
|
||||
assert.strictEqual(newData.creationDate, v1Data.creationDate);
|
||||
assert.strictEqual(newData.lastMessageDate, v1Data.creationDate);
|
||||
assert.strictEqual(newData.version, 3);
|
||||
assert.ok('customTitle' in newData);
|
||||
});
|
||||
|
||||
test('v2', () => {
|
||||
|
||||
@ -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>();
|
||||
@ -49,7 +49,7 @@ export class MockChatService implements IChatService {
|
||||
async getOrRestoreSession(sessionResource: URI): Promise<IChatModelReference | undefined> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getPersistedSessionTitle(sessionResource: URI): string | undefined {
|
||||
getSessionTitle(sessionResource: URI): string | undefined {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
loadSessionFromContent(data: ISerializableChatData): IChatModelReference | undefined {
|
||||
@ -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.');
|
||||
}
|
||||
|
||||
@ -124,10 +124,6 @@ export class MockChatService implements IChatService {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
isPersistedSessionEmpty(sessionResource: URI): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
activateDefaultAgent(location: ChatAgentLocation): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@ -7,16 +7,17 @@ import assert from 'assert';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { createManageTodoListToolData } from '../../../common/tools/manageTodoListTool.js';
|
||||
import { IToolData } from '../../../common/languageModelToolsService.js';
|
||||
import { IJSONSchema } from '../../../../../../base/common/jsonSchema.js';
|
||||
|
||||
suite('ManageTodoListTool Description Field Setting', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
function getSchemaProperties(toolData: IToolData): { properties: any; required: string[] } {
|
||||
assert.ok(toolData.inputSchema);
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
const schema = toolData.inputSchema as any;
|
||||
const properties = schema?.properties?.todoList?.items?.properties;
|
||||
const required = schema?.properties?.todoList?.items?.required;
|
||||
const schema = toolData.inputSchema;
|
||||
const todolistItems = schema?.properties?.todoList?.items as IJSONSchema | undefined;
|
||||
const properties = todolistItems?.properties;
|
||||
const required = todolistItems?.required;
|
||||
|
||||
assert.ok(properties, 'Schema properties should be defined');
|
||||
assert.ok(required, 'Schema required fields should be defined');
|
||||
|
||||
@ -60,6 +60,7 @@ import { DisassemblyViewInput } from '../common/disassemblyViewInput.js';
|
||||
import * as icons from './debugIcons.js';
|
||||
import { DisassemblyView } from './disassemblyView.js';
|
||||
import { equals } from '../../../../base/common/arrays.js';
|
||||
import { hasKey } from '../../../../base/common/types.js';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@ -1823,7 +1824,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated:
|
||||
}
|
||||
|
||||
const appendMessage = (text: string): string => {
|
||||
return ('message' in breakpoint && breakpoint.message) ? text.concat(', ' + breakpoint.message) : text;
|
||||
return breakpoint.message ? text.concat(', ' + breakpoint.message) : text;
|
||||
};
|
||||
|
||||
if (debugActive && breakpoint instanceof Breakpoint && breakpoint.pending) {
|
||||
@ -1835,7 +1836,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated:
|
||||
if (debugActive && !breakpoint.verified) {
|
||||
return {
|
||||
icon: breakpointIcon.unverified,
|
||||
message: ('message' in breakpoint && breakpoint.message) ? breakpoint.message : (breakpoint.logMessage ? localize('unverifiedLogpoint', "Unverified Logpoint") : localize('unverifiedBreakpoint', "Unverified Breakpoint")),
|
||||
message: breakpoint.message ? breakpoint.message : (breakpoint.logMessage ? localize('unverifiedLogpoint', "Unverified Logpoint") : localize('unverifiedBreakpoint', "Unverified Breakpoint")),
|
||||
showAdapterUnverifiedMessage: true
|
||||
};
|
||||
}
|
||||
@ -1935,7 +1936,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated:
|
||||
};
|
||||
}
|
||||
|
||||
const message = ('message' in breakpoint && breakpoint.message) ? breakpoint.message : breakpoint instanceof Breakpoint && labelService ? labelService.getUriLabel(breakpoint.uri) : localize('breakpoint', "Breakpoint");
|
||||
const message = breakpoint.message ? breakpoint.message : breakpoint instanceof Breakpoint && labelService ? labelService.getUriLabel(breakpoint.uri) : localize('breakpoint', "Breakpoint");
|
||||
return {
|
||||
icon: breakpointIcon.regular,
|
||||
message
|
||||
@ -2047,7 +2048,7 @@ abstract class MemoryBreakpointAction extends Action2 {
|
||||
}));
|
||||
disposables.add(input.onDidAccept(() => {
|
||||
const r = this.parseAddress(input.value, true);
|
||||
if ('error' in r) {
|
||||
if (hasKey(r, { error: true })) {
|
||||
input.validationMessage = r.error;
|
||||
} else {
|
||||
resolve(r);
|
||||
|
||||
@ -16,34 +16,6 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-resize-overlay {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--vscode-editorWidget-background);
|
||||
color: var(--vscode-editorWidget-foreground);
|
||||
border: 1px solid var(--vscode-editorWidget-border);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 80ms ease-out;
|
||||
z-index: 35;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.monaco-workbench.hc-black .terminal-resize-overlay,
|
||||
.monaco-workbench.hc-light .terminal-resize-overlay {
|
||||
box-shadow: none;
|
||||
border-color: var(--vscode-contrastBorder);
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-resize-overlay.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.terminal-command-decoration.hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@ -94,7 +94,6 @@ import { refreshShellIntegrationInfoStatus } from './terminalTooltip.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { PromptInputState } from '../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js';
|
||||
import { hasKey, isNumber, isString } from '../../../../base/common/types.js';
|
||||
import { TerminalResizeDimensionsOverlay } from './terminalResizeDimensionsOverlay.js';
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
@ -203,7 +202,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
private _lineDataEventAddon: LineDataEventAddon | undefined;
|
||||
private readonly _scopedContextKeyService: IContextKeyService;
|
||||
private _resizeDebouncer?: TerminalResizeDebouncer;
|
||||
private readonly _terminalResizeDimensionsOverlay: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
|
||||
|
||||
readonly capabilities = this._register(new TerminalCapabilityStoreMultiplexer());
|
||||
readonly statusList: ITerminalStatusList;
|
||||
@ -1182,17 +1180,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
}
|
||||
this.updateConfig();
|
||||
|
||||
// Initialize resize dimensions overlay
|
||||
this.processReady.then(() => {
|
||||
// Wait a second to avoid resize events during startup like when opening a terminal or
|
||||
// when a terminal reconnects. Ideally we'd have an actual event to listen to here.
|
||||
timeout(1000).then(() => {
|
||||
if (!this._store.isDisposed) {
|
||||
this._terminalResizeDimensionsOverlay.value = new TerminalResizeDimensionsOverlay(this._wrapperElement, xterm);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal
|
||||
// panel was initialized.
|
||||
if (xterm.raw.options.disableStdin) {
|
||||
|
||||
@ -28,6 +28,7 @@ import '../terminalContrib/stickyScroll/browser/terminal.stickyScroll.contributi
|
||||
import '../terminalContrib/quickAccess/browser/terminal.quickAccess.contribution.js';
|
||||
import '../terminalContrib/quickFix/browser/terminal.quickFix.contribution.js';
|
||||
import '../terminalContrib/typeAhead/browser/terminal.typeAhead.contribution.js';
|
||||
import '../terminalContrib/resizeDimensionsOverlay/browser/terminal.resizeDimensionsOverlay.contribution.js';
|
||||
import '../terminalContrib/sendSequence/browser/terminal.sendSequence.contribution.js';
|
||||
import '../terminalContrib/sendSignal/browser/terminal.sendSignal.contribution.js';
|
||||
import '../terminalContrib/suggest/browser/terminal.suggest.contribution.js';
|
||||
|
||||
@ -379,16 +379,10 @@ registerAction2(class ShowChatTerminalsAction extends Action2 {
|
||||
const chatSessionId = terminalChatService.getChatSessionIdForInstance(instance);
|
||||
let chatSessionTitle: string | undefined;
|
||||
if (chatSessionId) {
|
||||
const sessionUri = LocalChatSessionUri.forSession(chatSessionId);
|
||||
// Try to get title from active session first, then fall back to persisted title
|
||||
chatSessionTitle = chatService.getSession(sessionUri)?.title || chatService.getPersistedSessionTitle(sessionUri);
|
||||
}
|
||||
|
||||
let description: string | undefined;
|
||||
if (chatSessionTitle) {
|
||||
description = `${chatSessionTitle}`;
|
||||
chatSessionTitle = chatService.getSessionTitle(LocalChatSessionUri.forSession(chatSessionId));
|
||||
}
|
||||
|
||||
const description = chatSessionTitle;
|
||||
let detail: string | undefined;
|
||||
let tooltip: string | IMarkdownString | undefined;
|
||||
if (lastCommand) {
|
||||
|
||||
@ -260,6 +260,28 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
|
||||
find: true,
|
||||
'/^find\\b.*-(delete|exec|execdir|fprint|fprintf|fls|ok|okdir)\\b/': false,
|
||||
|
||||
// rg (ripgrep)
|
||||
// - `--pre`: Executes arbitrary command as preprocessor for every file searched.
|
||||
// - `--hostname-bin`: Executes arbitrary command to get hostname.
|
||||
rg: true,
|
||||
'/^rg\\b.*(--pre|--hostname-bin)\\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`/`-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|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,11 @@ suite('RunInTerminalTool', () => {
|
||||
'date +%Y-%m-%d',
|
||||
'find . -name "*.txt"',
|
||||
'grep pattern file.txt',
|
||||
'rg pattern file.txt',
|
||||
'rg --json pattern .',
|
||||
'rg -i --color=never "TODO" src/',
|
||||
'sed "s/foo/bar/g"',
|
||||
'sed -n "1,10p" file.txt',
|
||||
'sort file.txt',
|
||||
'tree directory'
|
||||
];
|
||||
@ -295,6 +300,19 @@ suite('RunInTerminalTool', () => {
|
||||
'find . -exec rm {} \\;',
|
||||
'find . -execdir rm {} \\;',
|
||||
'find . -fprint output.txt',
|
||||
'rg --pre cat pattern .',
|
||||
'rg --hostname-bin hostname pattern .',
|
||||
'sed -i "s/foo/bar/g" file.txt',
|
||||
'sed -i.bak "s/foo/bar/" file.txt',
|
||||
'sed -Ibak "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',
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .terminal-resize-overlay {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--vscode-editorWidget-background);
|
||||
color: var(--vscode-editorWidget-foreground);
|
||||
border: 1px solid var(--vscode-editorWidget-border);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 80ms ease-out;
|
||||
z-index: 35;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.monaco-workbench.hc-black .terminal-resize-overlay,
|
||||
.monaco-workbench.hc-light .terminal-resize-overlay {
|
||||
box-shadow: none;
|
||||
border-color: var(--vscode-contrastBorder);
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-resize-overlay.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { Terminal as RawXtermTerminal } from '@xterm/xterm';
|
||||
import { Disposable, MutableDisposable, type IDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import type { ITerminalContribution, IXtermTerminal } from '../../../terminal/browser/terminal.js';
|
||||
import { registerTerminalContribution, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js';
|
||||
import { timeout } from '../../../../../base/common/async.js';
|
||||
import { TerminalResizeDimensionsOverlay } from './terminalResizeDimensionsOverlay.js';
|
||||
|
||||
class TerminalResizeDimensionsOverlayContribution extends Disposable implements ITerminalContribution {
|
||||
static readonly ID = 'terminal.resizeDimensionsOverlay';
|
||||
|
||||
private readonly _overlay: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
|
||||
|
||||
constructor(
|
||||
private readonly _ctx: ITerminalContributionContext,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void {
|
||||
// Initialize resize dimensions overlay
|
||||
this._ctx.processManager.ptyProcessReady.then(() => {
|
||||
// Wait a second to avoid resize events during startup like when opening a terminal or
|
||||
// when a terminal reconnects. Ideally we'd have an actual event to listen to here.
|
||||
timeout(1000).then(() => {
|
||||
if (!this._store.isDisposed) {
|
||||
this._overlay.value = new TerminalResizeDimensionsOverlay(this._ctx.instance.domElement, xterm);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
registerTerminalContribution(TerminalResizeDimensionsOverlayContribution.ID, TerminalResizeDimensionsOverlayContribution);
|
||||
@ -3,10 +3,13 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $ } from '../../../../base/browser/dom.js';
|
||||
import { disposableTimeout } from '../../../../base/common/async.js';
|
||||
import { Disposable, MutableDisposable, toDisposable, type IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import type { XtermTerminal } from './xterm/xtermTerminal.js';
|
||||
|
||||
import './media/terminalResizeDimensionsOverlay.css';
|
||||
import { $ } from '../../../../../base/browser/dom.js';
|
||||
import { disposableTimeout } from '../../../../../base/common/async.js';
|
||||
import { Disposable, MutableDisposable, toDisposable, type IDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import type { IXtermTerminal } from '../../../terminal/browser/terminal.js';
|
||||
import type { XtermTerminal } from '../../../terminal/browser/xterm/xtermTerminal.js';
|
||||
|
||||
const enum Constants {
|
||||
ResizeOverlayHideDelay = 500,
|
||||
@ -20,11 +23,11 @@ export class TerminalResizeDimensionsOverlay extends Disposable {
|
||||
|
||||
constructor(
|
||||
private readonly _container: HTMLElement,
|
||||
xterm: XtermTerminal,
|
||||
xterm: IXtermTerminal,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(xterm.raw.onResize(dims => this._handleDimensionsChanged(dims)));
|
||||
this._register((xterm as XtermTerminal).raw.onResize(dims => this._handleDimensionsChanged(dims)));
|
||||
this._register(toDisposable(() => {
|
||||
this._resizeOverlay?.remove();
|
||||
this._resizeOverlay = undefined;
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,6 +241,14 @@ declare module 'vscode' {
|
||||
*/
|
||||
readonly onDidChangeChatSessionOptions?: Event<ChatSessionOptionChangeEvent>;
|
||||
|
||||
/**
|
||||
* Event that the provider can fire to signal that the available provider options have changed.
|
||||
*
|
||||
* When fired, the editor will re-query {@link ChatSessionContentProvider.provideChatSessionProviderOptions}
|
||||
* and update the UI to reflect the new option groups.
|
||||
*/
|
||||
readonly onDidChangeChatSessionProviderOptions?: Event<void>;
|
||||
|
||||
/**
|
||||
* Provides the chat session content for a given uri.
|
||||
*
|
||||
|
||||
@ -6,6 +6,6 @@
|
||||
declare module 'vscode' {
|
||||
|
||||
export namespace interactive {
|
||||
export function transferActiveChat(toWorkspace: Uri): void;
|
||||
export function transferActiveChat(toWorkspace: Uri): Thenable<void>;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user