Skip to content

Commit 0900bb8

Browse files
authored
Agent Mode pauses when VS Code loses focus (fix microsoft#252384) (microsoft#254827)
* Agent Mode pauses when VS Code loses focus (fix microsoft#252384) * add logging * refactor - rename throttling parameter to allowed * refactor - extend `IChatModel` with `IDisposable`
1 parent e52bb47 commit 0900bb8

File tree

9 files changed

+55
-4
lines changed

9 files changed

+55
-4
lines changed

src/vs/platform/native/common/native.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export interface ICommonNativeHostService {
130130

131131
saveWindowSplash(splash: IPartsSplash): Promise<void>;
132132

133+
setBackgroundThrottling(allowed: boolean): Promise<void>;
134+
133135
/**
134136
* Make the window focused.
135137
* @param options specify the specific window to focus and the focus mode.

src/vs/platform/native/electron-main/nativeHostMainService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
371371
this.themeMainService.saveWindowSplash(windowId, window?.openedWorkspace, splash);
372372
}
373373

374+
async setBackgroundThrottling(windowId: number | undefined, allowed: boolean): Promise<void> {
375+
const window = this.codeWindowById(windowId);
376+
377+
this.logService.trace(`Setting background throttling for window ${windowId} to '${allowed}'`);
378+
379+
window?.win?.webContents?.setBackgroundThrottling(allowed);
380+
}
381+
374382
//#endregion
375383

376384

src/vs/workbench/contrib/chat/common/chatModel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,7 @@ export interface IChatRequestDisablement {
901901
afterUndoStop?: string;
902902
}
903903

904-
export interface IChatModel {
904+
export interface IChatModel extends IDisposable {
905905
readonly onDidDispose: Event<void>;
906906
readonly onDidChange: Event<IChatChangeEvent>;
907907
readonly sessionId: string;

src/vs/workbench/contrib/chat/common/chatService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,8 @@ export interface IChatService {
574574
activateDefaultAgent(location: ChatAgentLocation): Promise<void>;
575575

576576
readonly edits2Enabled: boolean;
577+
578+
readonly requestInProgressObs: IObservable<boolean>;
577579
}
578580

579581
export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation';

src/vs/workbench/contrib/chat/common/chatServiceImpl.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js';
1313
import { Iterable } from '../../../../base/common/iterator.js';
1414
import { Disposable, DisposableMap, IDisposable } from '../../../../base/common/lifecycle.js';
1515
import { revive } from '../../../../base/common/marshalling.js';
16+
import { derived, IObservable, ObservableMap } from '../../../../base/common/observable.js';
1617
import { StopWatch } from '../../../../base/common/stopwatch.js';
1718
import { URI } from '../../../../base/common/uri.js';
1819
import { isLocation } from '../../../../editor/common/languages.js';
@@ -109,7 +110,7 @@ class CancellableRequest implements IDisposable {
109110
export class ChatService extends Disposable implements IChatService {
110111
declare _serviceBrand: undefined;
111112

112-
private readonly _sessionModels = this._register(new DisposableMap<string, ChatModel>());
113+
private readonly _sessionModels = new ObservableMap<string, ChatModel>();
113114
private readonly _pendingRequests = this._register(new DisposableMap<string, CancellableRequest>());
114115
private _persistedSessions: ISerializableChatsData;
115116

@@ -134,6 +135,8 @@ export class ChatService extends Disposable implements IChatService {
134135
private readonly _chatServiceTelemetry: ChatServiceTelemetry;
135136
private readonly _chatSessionStore: ChatSessionStore;
136137

138+
readonly requestInProgressObs: IObservable<boolean>;
139+
137140
@memoize
138141
private get useFileStorage(): boolean {
139142
return this.configurationService.getValue(ChatConfiguration.UseFileStorage);
@@ -196,6 +199,11 @@ export class ChatService extends Disposable implements IChatService {
196199
}
197200

198201
this._register(storageService.onWillSaveState(() => this.saveState()));
202+
203+
this.requestInProgressObs = derived(reader => {
204+
const models = this._sessionModels.observable.read(reader).values();
205+
return Array.from(models).some(model => model.requestInProgressObs.read(reader));
206+
});
199207
}
200208

201209
isEnabled(location: ChatAgentLocation): boolean {
@@ -1125,7 +1133,8 @@ export class ChatService extends Disposable implements IChatService {
11251133
}
11261134
}
11271135

1128-
this._sessionModels.deleteAndDispose(sessionId);
1136+
this._sessionModels.delete(sessionId);
1137+
model.dispose();
11291138
this._pendingRequests.get(sessionId)?.cancel();
11301139
this._pendingRequests.deleteAndDispose(sessionId);
11311140
this._onDidDisposeSession.fire({ sessionId, reason: 'cleared' });

src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import { IWorkbenchLayoutService } from '../../../services/layout/browser/layout
2727
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
2828
import { ChatContextKeys } from '../common/chatContextKeys.js';
2929
import { ViewContainerLocation } from '../../../common/views.js';
30+
import { INativeHostService } from '../../../../platform/native/common/native.js';
31+
import { IChatService } from '../common/chatService.js';
32+
import { autorun } from '../../../../base/common/observable.js';
3033

3134
class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchContribution {
3235

@@ -106,6 +109,27 @@ class ChatCommandLineHandler extends Disposable {
106109
}
107110
}
108111

112+
class ChatSuspendThrottlingHandler extends Disposable {
113+
114+
static readonly ID = 'workbench.contrib.chatSuspendThrottlingHandler';
115+
116+
constructor(
117+
@INativeHostService nativeHostService: INativeHostService,
118+
@IChatService chatService: IChatService
119+
) {
120+
super();
121+
122+
this._register(autorun(reader => {
123+
const running = chatService.requestInProgressObs.read(reader);
124+
125+
// When a chat request is in progress, we must ensure that background
126+
// throttling is not applied so that the chat session can continue
127+
// even when the window is not in focus.
128+
nativeHostService.setBackgroundThrottling(!running);
129+
}));
130+
}
131+
}
132+
109133
registerAction2(StartVoiceChatAction);
110134
registerAction2(InstallSpeechProviderForVoiceChatAction);
111135

@@ -126,3 +150,4 @@ registerChatDeveloperActions();
126150
registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored);
127151
registerWorkbenchContribution2(NativeBuiltinToolsContribution.ID, NativeBuiltinToolsContribution, WorkbenchPhase.AfterRestored);
128152
registerWorkbenchContribution2(ChatCommandLineHandler.ID, ChatCommandLineHandler, WorkbenchPhase.BlockRestore);
153+
registerWorkbenchContribution2(ChatSuspendThrottlingHandler.ID, ChatSuspendThrottlingHandler, WorkbenchPhase.AfterRestored);

src/vs/workbench/contrib/chat/test/common/chatService.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ suite('ChatService', () => {
287287
assert(chatModel2);
288288

289289
await assertSnapshot(toSnapshotExportData(chatModel2));
290+
chatModel2.dispose();
290291
});
291292

292293
test('can deserialize with response', async () => {
@@ -315,6 +316,7 @@ suite('ChatService', () => {
315316
assert(chatModel2);
316317

317318
await assertSnapshot(toSnapshotExportData(chatModel2));
319+
chatModel2.dispose();
318320
});
319321
});
320322

src/vs/workbench/contrib/chat/test/common/mockChatService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
import { CancellationToken } from '../../../../../base/common/cancellation.js';
77
import { Event } from '../../../../../base/common/event.js';
8+
import { observableValue } from '../../../../../base/common/observable.js';
89
import { URI } from '../../../../../base/common/uri.js';
910
import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../common/chatModel.js';
1011
import { IParsedChatRequest } from '../../common/chatParserTypes.js';
1112
import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from '../../common/chatService.js';
1213
import { ChatAgentLocation } from '../../common/constants.js';
1314

1415
export class MockChatService implements IChatService {
16+
requestInProgressObs = observableValue('name', false);
1517
edits2Enabled: boolean = false;
1618
_serviceBrand: undefined;
1719
transferredSessionData: IChatTransferredSessionData | undefined;

src/vs/workbench/test/electron-browser/workbenchTestServices.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,12 @@ export class TestNativeHostService implements INativeHostService {
104104
async isWindowAlwaysOnTop(options?: INativeHostOptions): Promise<boolean> { return false; }
105105
async toggleWindowAlwaysOnTop(options?: INativeHostOptions): Promise<void> { }
106106
async setWindowAlwaysOnTop(alwaysOnTop: boolean, options?: INativeHostOptions): Promise<void> { }
107-
getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { throw new Error('Method not implemented.'); }
107+
async getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { throw new Error('Method not implemented.'); }
108108
async positionWindow(position: IRectangle, options?: INativeHostOptions): Promise<void> { }
109109
async updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): Promise<void> { }
110110
async setMinimumSize(width: number | undefined, height: number | undefined): Promise<void> { }
111111
async saveWindowSplash(value: IPartsSplash): Promise<void> { }
112+
async setBackgroundThrottling(throttling: boolean): Promise<void> { }
112113
async focusWindow(options?: INativeHostOptions): Promise<void> { }
113114
async showMessageBox(options: Electron.MessageBoxOptions): Promise<Electron.MessageBoxReturnValue> { throw new Error('Method not implemented.'); }
114115
async showSaveDialog(options: Electron.SaveDialogOptions): Promise<Electron.SaveDialogReturnValue> { throw new Error('Method not implemented.'); }

0 commit comments

Comments
 (0)