Merge branch 'main' into dev/dmitriv/go-to-column

This commit is contained in:
Dmitriy Vasyura 2025-12-23 11:53:21 -08:00 committed by GitHub
commit 296108389a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 257 additions and 198 deletions

View File

@ -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',

View File

@ -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"
},

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -1476,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;
@ -1759,6 +1758,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this._onDidChangeHeight.fire();
}
}));
this.renderAttachedContext();
}
public toggleChatInputOverlay(editing: boolean): void {

View File

@ -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;
}
}

View File

@ -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: {

View File

@ -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,

View File

@ -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
}
@ -1024,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[];

View File

@ -76,7 +76,6 @@ 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 _transferredSessionResource: URI | undefined;
@ -125,7 +124,7 @@ export class ChatService extends Disposable implements IChatService {
}
constructor(
@IStorageService storageService: IStorageService,
@IStorageService private readonly storageService: IStorageService,
@ILogService private readonly logService: ILogService,
@IExtensionService private readonly extensionService: IExtensionService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ -160,20 +159,8 @@ 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 = {};
}
this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore));
this._chatSessionStore.migrateDataIfNeeded(() => this._persistedSessions);
this._chatSessionStore.migrateDataIfNeeded(() => this.migrateData());
const transferredData = this._chatSessionStore.getTransferredSessionData();
if (transferredData) {
@ -181,11 +168,7 @@ export class ChatService extends Disposable implements IChatService {
this._transferredSessionResource = transferredData;
}
// When using file storage, populate _persistedSessions with session metadata from the index
// This ensures that getPersistedSessionTitle() can find titles for inactive sessions
this.initializePersistedSessionsFromFileStorage().then(() => {
this.reviveSessionsWithEdits();
});
this.reviveSessionsWithEdits();
this._register(storageService.onWillSaveState(() => this.saveState()));
@ -205,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;
@ -264,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}`);
}
@ -304,7 +310,8 @@ export class ChatService extends Disposable implements IChatService {
* 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;
}
@ -319,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.
@ -536,55 +515,20 @@ export class ChatService extends Disposable implements IChatService {
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,
@ -844,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,

View File

@ -419,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);

View File

@ -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) {

View File

@ -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;
}
@ -158,10 +158,6 @@ class MockChatService implements IChatService {
logChatIndex(): void { }
isPersistedSessionEmpty(_sessionResource: URI): boolean {
return false;
}
activateDefaultAgent(_location: ChatAgentLocation): Promise<void> {
return Promise.resolve();
}

View File

@ -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', () => {

View File

@ -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 {
@ -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.');
}

View File

@ -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');

View File

@ -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);

View File

@ -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;
}

View File

@ -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) {

View File

@ -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';

View File

@ -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) {

View File

@ -271,7 +271,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
// while processing the input.
// - `-f`/`--file`: Add the commands contained in the file script-file to the set of
// commands to be run while processing the input.
// - `-i`/`--in-place`: This option specifies that files are to be edited in-place.
// - `-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
@ -279,7 +279,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
// - Note that `--sandbox` exists which blocks unsafe commands that could potentially be
// leveraged to auto approve
sed: true,
'/^sed\\b.*(-[a-zA-Z]*(e|i|f)[a-zA-Z]*|--expression|--file|--in-place)\\b/': false,
'/^sed\\b.*(-[a-zA-Z]*(e|i|I|f)[a-zA-Z]*|--expression|--file|--in-place)\\b/': false,
'/^sed\\b.*(\/e|\/w|;W)/': false,
// sort

View File

@ -304,6 +304,7 @@ suite('RunInTerminalTool', () => {
'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',

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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.
*