Merge remote-tracking branch 'origin/main' into dev/dmitriv/fetch-fixes-2

This commit is contained in:
Dmitriy Vasyura
2025-12-04 13:32:38 -08:00
16 changed files with 155 additions and 33 deletions
@@ -73,6 +73,13 @@ export class WebPageLoader extends Disposable {
.once('did-fail-load', this.onFailLoad.bind(this))
.once('will-navigate', this.onRedirect.bind(this))
.once('will-redirect', this.onRedirect.bind(this));
// Disable any UI interactions that could interfere with content loading.
this._window.webContents
.on('login', (event) => event.preventDefault())
.on('select-client-certificate', (event) => event.preventDefault())
.on('certificate-error', (event) => event.preventDefault());
}
private trace(message: string) {
@@ -165,7 +172,12 @@ export class WebPageLoader extends Disposable {
}
this.trace(`Received 'did-fail-load' event, code: ${statusCode}, error: '${error}'`);
void this._queue.queue(() => this.extractContent({ status: 'error', statusCode, error }));
if (statusCode === -3) {
this.trace(`Ignoring ERR_ABORTED (-3) as it may be caused by CSP or other measures`);
void this._queue.queue(() => this.extractContent());
} else {
void this._queue.queue(() => this.extractContent({ status: 'error', statusCode, error }));
}
}
/**
@@ -35,6 +35,14 @@ class MockWebContents {
return this;
}
on(event: string, listener: (...args: unknown[]) => void): this {
if (!this._listeners.has(event)) {
this._listeners.set(event, []);
}
this._listeners.get(event)!.push(listener);
return this;
}
emit(event: string, ...args: unknown[]): void {
const listeners = this._listeners.get(event) || [];
for (const listener of listeners) {
@@ -5,7 +5,7 @@
import { raceCancellationError } from '../../../base/common/async.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { Emitter } from '../../../base/common/event.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js';
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../base/common/map.js';
@@ -16,9 +16,10 @@ import { URI, UriComponents } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
import { ILogService } from '../../../platform/log/common/log.js';
import { ChatViewPaneTarget, IChatWidgetService, isIChatViewViewContext } from '../../contrib/chat/browser/chat.js';
import { IChatEditorOptions } from '../../contrib/chat/browser/chatEditor.js';
import { ChatEditorInput } from '../../contrib/chat/browser/chatEditorInput.js';
import { ChatViewPaneTarget, IChatWidgetService, isIChatViewViewContext } from '../../contrib/chat/browser/chat.js';
import { awaitStatsForSession } from '../../contrib/chat/common/chat.js';
import { IChatAgentRequest } from '../../contrib/chat/common/chatAgents.js';
import { IChatContentInlineReference, IChatProgress, IChatService } from '../../contrib/chat/common/chatService.js';
import { IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js';
@@ -342,7 +343,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
this._proxy = this._extHostContext.getProxy(ExtHostContext.ExtHostChatSessions);
this._chatSessionsService.setOptionsChangeCallback(async (sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>) => {
this._chatSessionsService.setOptionsChangeCallback(async (sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>) => {
const handle = this._getHandleForSessionType(sessionResource.scheme);
if (handle !== undefined) {
await this.notifyOptionsChange(handle, sessionResource, updates);
@@ -360,7 +361,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
const changeEmitter = disposables.add(new Emitter<void>());
const provider: IChatSessionItemProvider = {
chatSessionType,
onDidChangeChatSessionItems: changeEmitter.event,
onDidChangeChatSessionItems: Event.debounce(changeEmitter.event, (_, e) => e, 200),
provideChatSessionItems: (token) => this._provideChatSessionItems(handle, token),
provideNewChatSessionItem: (options, token) => this._provideNewChatSessionItem(handle, options, token)
};
@@ -448,21 +449,35 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
try {
// Get all results as an array from the RPC call
const sessions = await this._proxy.$provideChatSessionItems(handle, token);
return sessions.map(session => {
return Promise.all(sessions.map(async session => {
const uri = URI.revive(session.resource);
const model = this._chatService.getSession(uri);
let description: string | undefined;
let statistics: IChatSessionItem['statistics'];
if (model) {
description = this._chatSessionsService.getSessionDescription(model);
}
const modelStats = model ?
await awaitStatsForSession(model) :
(await this._chatService.getMetadataForSession(uri))?.stats;
if (modelStats) {
statistics = {
files: modelStats.fileCount,
insertions: modelStats.added,
deletions: modelStats.removed
};
}
return {
...session,
resource: uri,
iconPath: session.iconPath,
tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined,
description: description || session.description
};
});
description: description || session.description,
statistics
} satisfies IChatSessionItem;
}));
} catch (error) {
this._logService.error('Error providing chat sessions:', error);
}
@@ -629,7 +644,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
/**
* Notify the extension about option changes for a session
*/
async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | undefined }>): Promise<void> {
async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>): Promise<void> {
try {
await this._proxy.$provideHandleOptionsChange(handle, sessionResource, updates, CancellationToken.None);
} catch (error) {
@@ -3275,12 +3275,12 @@ export type IChatSessionHistoryItemDto = {
export interface ChatSessionOptionUpdateDto {
readonly optionId: string;
readonly value: string | undefined;
readonly value: string | IChatSessionProviderOptionItem | undefined;
}
export interface ChatSessionOptionUpdateDto2 {
readonly optionId: string;
readonly value: string;
readonly value: string | IChatSessionProviderOptionItem;
}
export interface ChatSessionDto {
@@ -15,7 +15,7 @@ import { URI, UriComponents } from '../../../base/common/uri.js';
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { ILogService } from '../../../platform/log/common/log.js';
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';
import { ChatSessionStatus, IChatSessionItem } from '../../contrib/chat/common/chatSessionsService.js';
import { ChatSessionStatus, IChatSessionItem, IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js';
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
import { ChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, IChatSessionProviderOptions, MainContext, MainThreadChatSessionsShape } from './extHost.protocol.js';
@@ -309,7 +309,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
};
}
async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string | undefined }>, token: CancellationToken): Promise<void> {
async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>, token: CancellationToken): Promise<void> {
const sessionResource = URI.revive(sessionResourceComponents);
const provider = this._chatSessionContentProviders.get(handle);
if (!provider) {
@@ -323,7 +323,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}
try {
await provider.provider.provideHandleOptionsChange(sessionResource, updates, token);
const updatesToSend = updates.map(update => ({
optionId: update.optionId,
value: update.value === undefined ? undefined : (typeof update.value === 'string' ? update.value : update.value.id)
}));
await provider.provider.provideHandleOptionsChange(sessionResource, updatesToSend, token);
} catch (error) {
this._logService.error(`Error calling provideHandleOptionsChange for handle ${handle}, sessionResource ${sessionResource}:`, error);
}
@@ -444,7 +444,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
// React to chat session option changes for the active session
this._register(this.chatSessionsService.onDidChangeSessionOptions(e => {
const sessionResource = this._widget?.viewModel?.model.sessionResource;
if (sessionResource && isEqual(sessionResource, e.resource)) {
if (sessionResource && isEqual(sessionResource, e)) {
// Options changed for our current session - refresh pickers
this.refreshChatSessionPickers();
}
@@ -710,7 +710,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.getOrCreateOptionEmitter(optionGroup.id).fire(option);
this.chatSessionsService.notifySessionOptionsChange(
ctx.chatSessionResource,
[{ optionId: optionGroup.id, value: option.id }]
[{ optionId: optionGroup.id, value: option }]
).catch(err => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err));
},
getAllOptions: () => {
@@ -1270,9 +1270,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
if (currentOption) {
const optionGroup = optionGroups.find(g => g.id === optionGroupId);
if (optionGroup) {
const item = optionGroup.items.find(m => m.id === currentOption);
const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id;
const item = optionGroup.items.find(m => m.id === currentOptionId);
if (item) {
this.getOrCreateOptionEmitter(optionGroupId).fire(item);
// If currentOption is an object (not a string ID), it represents a complete option item and should be used directly.
// Otherwise, if it's a string ID, look up the corresponding item and use that.
if (typeof currentOption === 'string') {
this.getOrCreateOptionEmitter(optionGroupId).fire(item);
} else {
this.getOrCreateOptionEmitter(optionGroupId).fire(currentOption);
}
}
}
}
@@ -265,7 +265,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
private readonly _onDidChangeContentProviderSchemes = this._register(new Emitter<{ readonly added: string[]; readonly removed: string[] }>());
public get onDidChangeContentProviderSchemes() { return this._onDidChangeContentProviderSchemes.event; }
private readonly _onDidChangeSessionOptions = this._register(new Emitter<{ readonly resource: URI; readonly updates: ReadonlyArray<{ optionId: string; value: string }> }>());
private readonly _onDidChangeSessionOptions = this._register(new Emitter<URI>());
public get onDidChangeSessionOptions() { return this._onDidChangeSessionOptions.event; }
private readonly inProgressMap: Map<string, number> = new Map();
@@ -1078,7 +1078,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
/**
* Notify extension about option changes for a session
*/
public async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise<void> {
public async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise<void> {
if (!updates.length) {
return;
}
@@ -1088,7 +1088,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
for (const u of updates) {
this.setSessionOption(sessionResource, u.optionId, u.value);
}
this._onDidChangeSessionOptions.fire({ resource: sessionResource, updates });
this._onDidChangeSessionOptions.fire(sessionResource);
}
/**
@@ -1053,6 +1053,7 @@ export interface IChatService {
logChatIndex(): void;
getLiveSessionItems(): Promise<IChatDetail[]>;
getHistorySessionItems(): Promise<IChatDetail[]>;
getMetadataForSession(sessionResource: URI): Promise<IChatDetail | undefined>;
readonly onDidPerformUserAction: Event<IChatUserActionEvent>;
notifyUserAction(event: IChatUserActionEvent): void;
@@ -39,7 +39,7 @@ import { ChatRequestParser } from './chatRequestParser.js';
import { ChatMcpServersStarting, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js';
import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js';
import { IChatSessionsService } from './chatSessionsService.js';
import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js';
import { ChatSessionStore, IChatSessionEntryMetadata, IChatTransfer2 } from './chatSessionStore.js';
import { IChatSlashCommandService } from './chatSlashCommands.js';
import { IChatTransferService } from './chatTransferService.js';
import { LocalChatSessionUri } from './chatUri.js';
@@ -153,6 +153,8 @@ export class ChatService extends Disposable implements IChatService {
} else if (this._saveModelsEnabled) {
await this._chatSessionStore.storeSessions([model]);
}
} else if (!localSessionId && model.getRequests().length > 0) {
await this._chatSessionStore.storeSessionsMetadataOnly([model]);
}
}
}));
@@ -217,10 +219,14 @@ export class ChatService extends Disposable implements IChatService {
return;
}
const liveChats = Array.from(this._sessionModels.values())
const liveLocalChats = Array.from(this._sessionModels.values())
.filter(session => this.shouldStoreSession(session));
this._chatSessionStore.storeSessions(liveChats);
this._chatSessionStore.storeSessions(liveLocalChats);
const liveNonLocalChats = Array.from(this._sessionModels.values())
.filter(session => !LocalChatSessionUri.parseLocalSessionId(session.sessionResource));
this._chatSessionStore.storeSessionsMetadataOnly(liveNonLocalChats);
}
/**
@@ -405,18 +411,32 @@ export class ChatService extends Disposable implements IChatService {
async getHistorySessionItems(): Promise<IChatDetail[]> {
const index = await this._chatSessionStore.getIndex();
return Object.values(index)
.filter(entry => !entry.isExternal)
.filter(entry => !this._sessionModels.has(LocalChatSessionUri.forSession(entry.sessionId)) && entry.initialLocation === ChatAgentLocation.Chat && !entry.isEmpty)
.map((entry): IChatDetail => {
const sessionResource = LocalChatSessionUri.forSession(entry.sessionId);
return ({
...entry,
sessionResource,
stats: entry.stats,
isActive: this._sessionModels.has(sessionResource),
});
});
}
async getMetadataForSession(sessionResource: URI): Promise<IChatDetail | undefined> {
const index = await this._chatSessionStore.getIndex();
const metadata: IChatSessionEntryMetadata | undefined = index[sessionResource.toString()];
if (metadata) {
return {
...metadata,
sessionResource,
isActive: this._sessionModels.has(sessionResource),
};
}
return undefined;
}
private shouldBeInHistory(entry: ChatModel): boolean {
return !entry.isImported && !!LocalChatSessionUri.parseLocalSessionId(entry.sessionResource) && entry.initialLocation === ChatAgentLocation.Chat;
}
@@ -24,6 +24,7 @@ import { awaitStatsForSession } from './chat.js';
import { ModifiedFileEntryState } from './chatEditingService.js';
import { ChatModel, IChatModelInputState, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData } from './chatModel.js';
import { IChatSessionStats } from './chatService.js';
import { LocalChatSessionUri } from './chatUri.js';
import { ChatAgentLocation } from './constants.js';
const maxPersistedSessions = 25;
@@ -102,6 +103,27 @@ export class ChatSessionStore extends Disposable {
}
}
async storeSessionsMetadataOnly(sessions: ChatModel[]): Promise<void> {
if (this.shuttingDown) {
// Don't start this task if we missed the chance to block shutdown
return;
}
try {
this.storeTask = this.storeQueue.queue(async () => {
try {
await Promise.all(sessions.map(session => this.writeSessionMetadataOnly(session)));
await this.flushIndex();
} catch (e) {
this.reportError('storeSessions', 'Error storing chat sessions', e);
}
});
await this.storeTask;
} finally {
this.storeTask = undefined;
}
}
// async storeTransferSession(transferData: IChatTransfer, session: ISerializableChatData): Promise<void> {
// try {
// const content = JSON.stringify(session, undefined, 2);
@@ -144,6 +166,23 @@ export class ChatSessionStore extends Disposable {
}
}
private async writeSessionMetadataOnly(session: ChatModel): Promise<void> {
// Only to be used for external sessions
if (LocalChatSessionUri.parseLocalSessionId(session.sessionResource)) {
return;
}
try {
const index = this.internalGetIndex();
// TODO get this class on sessionResource
const externalSessionId = session.sessionResource.toString();
index.entries[externalSessionId] = await getSessionMetadata(session);
} catch (e) {
this.reportError('sessionMetadataWrite', 'Error writing chat session metadata', e);
}
}
private async flushIndex(): Promise<void> {
const index = this.internalGetIndex();
try {
@@ -163,6 +202,7 @@ export class ChatSessionStore extends Disposable {
private async trimEntries(): Promise<void> {
const index = this.internalGetIndex();
const entries = Object.entries(index.entries)
.filter(([_id, entry]) => !entry.isExternal)
.sort((a, b) => b[1].lastMessageDate - a[1].lastMessageDate)
.map(([id]) => id);
@@ -400,6 +440,11 @@ export interface IChatSessionEntryMetadata {
* filter the old ones out of history.
*/
isEmpty?: boolean;
/**
* Whether this session was loaded from an external provider (eg background/cloud sessions).
*/
isExternal?: boolean;
}
function isChatSessionEntryMetadata(obj: unknown): obj is IChatSessionEntryMetadata {
@@ -459,7 +504,8 @@ async function getSessionMetadata(session: ChatModel | ISerializableChatData): P
initialLocation: session.initialLocation,
hasPendingEdits: session instanceof ChatModel ? (session.editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified)) : false,
isEmpty: session instanceof ChatModel ? session.getRequests().length === 0 : session.requests.length === 0,
stats
stats,
isExternal: session instanceof ChatModel && !LocalChatSessionUri.parseLocalSessionId(session.sessionResource)
};
}
@@ -150,7 +150,7 @@ export interface IChatSessionContentProvider {
export type SessionOptionsChangedCallback = (sessionResource: URI, updates: ReadonlyArray<{
optionId: string;
value: string;
value: string | IChatSessionProviderOptionItem;
}>) => Promise<void>;
export interface IChatSessionsService {
@@ -203,7 +203,7 @@ export interface IChatSessionsService {
/**
* Fired when options for a chat session change.
*/
onDidChangeSessionOptions: Event<{ readonly resource: URI; readonly updates: ReadonlyArray<{ optionId: string; value: string }> }>;
onDidChangeSessionOptions: Event<URI>;
/**
* Get the capabilities for a specific session type
@@ -213,7 +213,7 @@ export interface IChatSessionsService {
getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined;
setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void;
setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void;
notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise<void>;
notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise<void>;
// Editable session support
setEditableSession(sessionResource: URI, data: IEditableData | null): Promise<void>;
@@ -181,6 +181,10 @@ class MockChatService implements IChatService {
waitForModelDisposals(): Promise<void> {
return Promise.resolve();
}
getMetadataForSession(sessionResource: URI): Promise<IChatDetail | undefined> {
throw new Error('Method not implemented.');
}
}
function createMockChatModel(options: {
@@ -146,4 +146,7 @@ export class MockChatService implements IChatService {
waitForModelDisposals(): Promise<void> {
throw new Error('Method not implemented.');
}
getMetadataForSession(sessionResource: URI): Promise<IChatDetail | undefined> {
throw new Error('Method not implemented.');
}
}
@@ -18,7 +18,7 @@ import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessi
export class MockChatSessionsService implements IChatSessionsService {
_serviceBrand: undefined;
private readonly _onDidChangeSessionOptions = new Emitter<{ readonly resource: URI; readonly updates: ReadonlyArray<{ optionId: string; value: string }> }>();
private readonly _onDidChangeSessionOptions = new Emitter<URI>();
readonly onDidChangeSessionOptions = this._onDidChangeSessionOptions.event;
private readonly _onDidChangeItemsProviders = new Emitter<IChatSessionItemProvider>();
readonly onDidChangeItemsProviders = this._onDidChangeItemsProviders.event;
@@ -306,7 +306,9 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
const quickSuggestionsConfig = this._configurationService.getValue<ITerminalSuggestConfiguration>(terminalSuggestConfigSection).quickSuggestions;
const allowFallbackCompletions = explicitlyInvoked || quickSuggestionsConfig.unknown === 'on';
this._logService.trace('SuggestAddon#_handleCompletionProviders provideCompletions');
const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.value, this._currentPromptInputState.cursorIndex, allowFallbackCompletions, this.shellType, this._capabilities, token, false, doNotRequestExtensionCompletions, explicitlyInvoked);
// Trim ghost text from the prompt value when requesting completions
const promptValue = this._mostRecentPromptInputState?.ghostTextIndex !== undefined ? this._currentPromptInputState.value.substring(0, this._mostRecentPromptInputState?.ghostTextIndex) : this._currentPromptInputState.value;
const providedCompletions = await this._terminalCompletionService.provideCompletions(promptValue, this._currentPromptInputState.cursorIndex, allowFallbackCompletions, this.shellType, this._capabilities, token, false, doNotRequestExtensionCompletions, explicitlyInvoked);
this._logService.trace('SuggestAddon#_handleCompletionProviders provideCompletions done');
if (token.isCancellationRequested) {
+1 -1
View File
@@ -199,7 +199,7 @@ declare module 'vscode' {
/**
* The new value assigned to the option. When `undefined`, the option is cleared.
*/
readonly value: string;
readonly value: string | ChatSessionProviderOptionItem;
}>;
}