Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-tag-paths-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"src/vs/workbench/contrib/positronRuntimeSessions/": ["@:sessions"],
"src/vs/workbench/contrib/positronSession/": ["@:sessions"],

"src/vs/workbench/services/chat/": ["@:assistant"],
"src/vs/workbench/services/languageRuntime/": ["@:console", "@:interpreter"],
"src/vs/workbench/services/runtimeSession/": ["@:sessions", "@:console", "@:interpreter"],
"src/vs/workbench/services/runtimeStartup/": ["@:console", "@:interpreter"],
Expand Down
40 changes: 20 additions & 20 deletions extensions/authentication/package.nls.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ import { captureExpectedAbortCommandId, captureExpectedConfirmCommandId, capture
import { InlineEditLogger } from './parts/inlineEditLogger';
import { VSCodeWorkspace } from './parts/vscodeWorkspace';
import { makeSettable } from './utils/observablesUtils';
// --- Start Positron ---
import { Emitter } from '../../../util/vs/base/common/event';
// --- End Positron ---

export class JointCompletionsProviderContribution extends Disposable implements IExtensionContribution {

Expand Down Expand Up @@ -101,7 +104,45 @@ export class JointCompletionsProviderContribution extends Disposable implements

const useJointCompletionsProviderObs = _configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderEnabled, _expService);

// --- Start Positron ---
// This is the single entry point for all Copilot inline suggestion
// registration (inline completions and Next Edit Suggestions, in both
// the joint and fallback paths). Gate it on Positron's AI master switch
// (ai.enabled) and the GitHub Copilot provider enable setting, so every
// Copilot suggestion is blocked when either is off, matching chat.
//
// ai.enabled is also checked at extension activation, but that only
// covers startup; reading it live here also handles runtime toggles
// (ai.enabled is permit-only, default true). The provider setting
// defaults on, so this only blocks when a setting is explicitly off (or
// enforced off via Workbench). The `positron.assistant` prefixed key is
// the one declared for Copilot; see extensions/authentication/package.json.
const suggestionsAllowedEmitter = this._register(new Emitter<void>());
this._register(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('ai.enabled') || e.affectsConfiguration('positron.assistant.provider.githubCopilot.enable')) {
suggestionsAllowedEmitter.fire();
}
}));
const copilotSuggestionsAllowed = observableFromEvent(
this,
suggestionsAllowedEmitter.event,
() => {
const aiEnabled = vscode.workspace.getConfiguration().get<boolean>('ai.enabled') !== false;
const providerEnabled = vscode.workspace.getConfiguration('positron.assistant.provider.githubCopilot').get<boolean>('enable') ?? true;
return aiEnabled && providerEnabled;
}
);
// --- End Positron ---

this._register(autorun((reader) => { // FX
// --- Start Positron ---
// Register no Copilot inline suggestion providers when the AI master
// switch or the Copilot provider is off. The autorun re-runs when
// either setting changes.
if (!copilotSuggestionsAllowed.read(reader)) {
return;
}
// --- End Positron ---
const useJointCompletionsProvider = useJointCompletionsProviderObs.read(reader);
if (!useJointCompletionsProvider) {
reader.store.add(_instantiationService.createInstance(InlineEditProviderFeatureContribution));
Expand Down
7 changes: 1 addition & 6 deletions product.json
Original file line number Diff line number Diff line change
Expand Up @@ -692,12 +692,7 @@
]
},
"trustedExtensionAuthAccess": {
"github": [
"GitHub.copilot-chat",
"positron.positron-assistant",
"posit.assistant",
"positron.authentication"
],
"github": [],
"github-enterprise": [
"GitHub.copilot-chat"
],
Expand Down
29 changes: 29 additions & 0 deletions src/vs/editor/common/services/completionsEnablement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ import { IConfigurationService } from '../../../platform/configuration/common/co
import { ITextResourceConfigurationService } from './textResourceConfiguration.js';
import { URI } from '../../../base/common/uri.js';

// --- Start Positron ---
// Positron's main AI switch. When off, all AI features (inline completions
// included) are off, regardless of `github.copilot.enable`. Defined as a literal
// because the editor layer can't import the workbench-level `AI_ENABLED_KEY`.
const AI_ENABLED_SETTING = 'ai.enabled';
// GitHub Copilot's provider enable setting. When off, Copilot completions are
// disabled (the provider isn't registered), so treat them as disabled here too
// -- this is what the completions status bar and the `areCompletionsEnabled`
// API read. Literal because the editor layer can't import the authentication
// extension's constant. Default is on, so only an explicit `false` disables.
const COPILOT_PROVIDER_ENABLE_SETTING = 'positron.assistant.provider.githubCopilot.enable';
// --- End Positron ---

/**
* Get the completions enablement setting name from product configuration.
*/
Expand All @@ -25,6 +38,14 @@ function getCompletionsEnablementSettingName(): string | undefined {
* @returns `true` if completions are enabled for the language, `false` otherwise.
*/
export function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean {
// --- Start Positron ---
if (configurationService.getValue(AI_ENABLED_SETTING) === false) {
return false; // main AI switch off
}
if (configurationService.getValue(COPILOT_PROVIDER_ENABLE_SETTING) === false) {
return false; // GitHub Copilot provider disabled
}
// --- End Positron ---
const settingName = getCompletionsEnablementSettingName();
if (!settingName) {
return false;
Expand All @@ -45,6 +66,14 @@ export function isCompletionsEnabled(configurationService: IConfigurationService
* @returns `true` if completions are enabled for the language, `false` otherwise.
*/
export function isCompletionsEnabledWithTextResourceConfig(configurationService: ITextResourceConfigurationService, resource: URI, modeId: string = '*'): boolean {
// --- Start Positron ---
if (configurationService.getValue<boolean>(resource, AI_ENABLED_SETTING) === false) {
return false; // main AI switch off
}
if (configurationService.getValue<boolean>(resource, COPILOT_PROVIDER_ENABLE_SETTING) === false) {
return false; // GitHub Copilot provider disabled
}
// --- End Positron ---
const settingName = getCompletionsEnablementSettingName();
if (!settingName) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,20 @@ import { IDefaultAccountService } from '../../../../../platform/defaultAccount/c
import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js';
import { Schemas } from '../../../../../base/common/network.js';
import { getInlineCompletionsController } from '../controller/common.js';
// --- Start Positron ---
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
// --- End Positron ---

export class InlineCompletionsModel extends Disposable {
private readonly _source;
// --- Start Positron ---
// Positron's main AI switch. When off, no AI inline completion provider (Copilot,
// Posit Assistant, etc.) may run; see `getAvailableProviders`. Defaults to true,
// so an unset value leaves completions enabled. Assigned in the constructor since
// it needs the injected configuration service.
private readonly _aiEnabled;
// --- End Positron ---
private readonly _isActive = observableValue<boolean>(this, false);
private readonly _onlyRequestInlineEditsSignal = observableSignal(this);
private readonly _forceUpdateExplicitlySignal = observableSignal(this);
Expand Down Expand Up @@ -121,8 +132,14 @@ export class InlineCompletionsModel extends Disposable {
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService,
@IDefaultAccountService defaultAccountService: IDefaultAccountService,
// --- Start Positron ---
@IConfigurationService private readonly _configurationService: IConfigurationService,
// --- End Positron ---
) {
super();
// --- Start Positron ---
this._aiEnabled = observableConfigValue<boolean>('ai.enabled', true, this._configurationService);
// --- End Positron ---
this._source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue, this.primaryPosition));
this.lastTriggerKind = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind);

Expand Down Expand Up @@ -370,6 +387,9 @@ export class InlineCompletionsModel extends Disposable {
this._onlyRequestInlineEditsSignal.read(reader);
this._forceUpdateExplicitlySignal.read(reader);
this._fetchSpecificProviderSignal.read(reader);
// --- Start Positron ---
this._aiEnabled.read(reader); // re-run this fetch when `ai.enabled` toggles, so turning the switch off clears completions immediately
// --- End Positron ---
const shouldUpdate = ((this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader))
&& (!this._inlineCompletionsService.isSnoozing() || changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit);
if (!shouldUpdate) {
Expand Down Expand Up @@ -460,6 +480,18 @@ export class InlineCompletionsModel extends Disposable {
// TODO: This is not an ideal implementation of excludesGroupIds, however as this is currently still behind proposed API
// and due to the time constraints, we are using a simplified approach
private getAvailableProviders(providers: InlineCompletionsProvider[]): InlineCompletionsProvider[] {
// --- Start Positron ---
// Positron's main AI switch turns off all AI inline completions: with no
// providers, nothing is fetched (Copilot, suggest-widget-context completions,
// and any AI augmentation of the selected suggest item all draw from these).
// The standard `editor.suggest.preview` ghost text (the inline preview of the
// selected IntelliSense item) is computed from the suggest selection, not from
// these providers, so it keeps working. The fetch derived reads `_aiEnabled`,
// so this recomputes when the switch toggles at runtime.
if (this._aiEnabled.get() === false) {
return [];
}
// --- End Positron ---
const suppressedProviderGroupIds = this._suppressedInlineCompletionGroupIds.get();
const unsuppressedProviders = providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId)));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsMod
import { IWithAsyncTestCodeEditorAndInlineCompletionsModel, MockInlineCompletionsProvider, withAsyncTestCodeEditorAndInlineCompletionsModel } from './utils.js';
import { ITestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
import { Selection } from '../../../../common/core/selection.js';
// --- Start Positron ---
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
// --- End Positron ---

suite('Inline Completions', () => {
ensureNoDisposablesAreLeakedInTestSuite();
Expand All @@ -30,6 +35,33 @@ suite('Inline Completions', () => {
);
});

// --- Start Positron ---
// Positron's `ai.enabled` main switch turns off all AI inline completions. When
// it is off, `InlineCompletionsModel.getAvailableProviders` returns no providers,
// so nothing is fetched even on an explicit trigger. This covers non-Copilot
// providers too (the Copilot-specific `isCompletionsEnabled` gate is tested
// separately in completionsEnablement.vitest.ts).
test('[Positron] Does not fetch completions when the ai.enabled main switch is off', async function () {
const provider = new MockInlineCompletionsProvider();
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration('ai.enabled', false);
const serviceCollection = new ServiceCollection([IConfigurationService, configurationService]);
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider, serviceCollection },
async ({ model, context }) => {
context.keyboardType('foo');
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) });
model.triggerExplicitly();
await timeout(1000);

// Provider is never called and no ghost text is shown.
assert.deepStrictEqual(provider.getAndClearCallHistory(), []);
assert.deepStrictEqual(context.getAndClearViewStates(), ['']);
}
);
});
// --- End Positron ---

test('Ghost text is shown after trigger', async function () {
const provider = new MockInlineCompletionsProvider();
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
Expand Down
124 changes: 124 additions & 0 deletions src/vs/editor/test/common/services/completionsEnablement.vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2026 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

/// <reference types="vitest/globals" />

import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';
import { ITextResourceConfigurationService } from '../../../common/services/textResourceConfiguration.js';
import { stubInterface } from '../../../../test/vitest/stubInterface.js';
import { URI } from '../../../../base/common/uri.js';

// Positron hides the Copilot chat UI via `chat.disableAIFeatures` but keeps inline
// completions working. Completions are gated only by their own setting
// (`github.copilot.enable`), never by `chat.disableAIFeatures`. These tests guard
// that independence. The product module is mocked so `completionsEnablementSetting`
// resolves in the test environment (the real product.json isn't loaded under vitest).
const { completionsSetting } = vi.hoisted(() => ({ completionsSetting: 'github.copilot.enable' }));
vi.mock('../../../../platform/product/common/product.js', async (importOriginal) => {
// Keep the rest of the product config (other modules read it at load time) and
// only ensure `completionsEnablementSetting` resolves under vitest.
const actual = await importOriginal<typeof import('../../../../platform/product/common/product.js')>();
return {
default: { ...actual.default, defaultChatAgent: { ...actual.default.defaultChatAgent, completionsEnablementSetting: completionsSetting } }
};
});

const { isCompletionsEnabled, isCompletionsEnabledWithTextResourceConfig } = await import('../../../common/services/completionsEnablement.js');

// A minimal ITextResourceConfigurationService backed by a TestConfigurationService,
// so the resource-scoped tests read the same settings the same way as the plain
// ones. The source only ever calls the two-arg `getValue(resource, section)` form.
function textResourceConfig(config: TestConfigurationService): ITextResourceConfigurationService {
return stubInterface<ITextResourceConfigurationService>({
getValue: ((_resource: URI | undefined, section?: string) => config.getValue(section)) as ITextResourceConfigurationService['getValue'],
});
}

const resource = URI.parse('file:///test.py');

describe('completions enablement is independent of chat.disableAIFeatures', () => {
for (const completionsOn of [true, false]) {
it(`tracks github.copilot.enable (${completionsOn}) regardless of chat.disableAIFeatures`, () => {
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration(completionsSetting, { '*': completionsOn });

configurationService.setUserConfiguration('chat.disableAIFeatures', false);
const withChatEnabled = isCompletionsEnabled(configurationService);

configurationService.setUserConfiguration('chat.disableAIFeatures', true);
const withChatDisabled = isCompletionsEnabled(configurationService);

// Same as the completions setting in both chat states.
expect([withChatEnabled, withChatDisabled]).toEqual([completionsOn, completionsOn]);
});
}
});

// The `ai.enabled` main switch has higher precedence than `github.copilot.enable`:
// when it's off, completions are off even if the completions setting is on.
describe('completions enablement respects the ai.enabled main switch', () => {
it('is off when ai.enabled is false, even with github.copilot.enable on', () => {
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration(completionsSetting, { '*': true });
configurationService.setUserConfiguration('ai.enabled', false);

expect(isCompletionsEnabled(configurationService)).toBe(false);
});

it('follows github.copilot.enable when ai.enabled is unset or true', () => {
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration(completionsSetting, { '*': true });

const whenUnset = isCompletionsEnabled(configurationService);
configurationService.setUserConfiguration('ai.enabled', true);
const whenOn = isCompletionsEnabled(configurationService);

expect([whenUnset, whenOn]).toEqual([true, true]);
});
});

// The GitHub Copilot provider enable setting disables completions when off:
// turning off the Copilot provider stops completions (the provider isn't
// registered), and the status bar / areCompletionsEnabled must agree.
describe('completions enablement respects the Copilot provider setting', () => {
const providerSetting = 'positron.assistant.provider.githubCopilot.enable';

it('is off when the Copilot provider is disabled, even with github.copilot.enable on', () => {
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration(completionsSetting, { '*': true });
configurationService.setUserConfiguration(providerSetting, false);

expect(isCompletionsEnabled(configurationService)).toBe(false);
});

it('follows github.copilot.enable when the Copilot provider is unset or enabled', () => {
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration(completionsSetting, { '*': true });

const whenUnset = isCompletionsEnabled(configurationService);
configurationService.setUserConfiguration(providerSetting, true);
const whenOn = isCompletionsEnabled(configurationService);

expect([whenUnset, whenOn]).toEqual([true, true]);
});

// isCompletionsEnabledWithTextResourceConfig is the per-file variant (it reads
// config scoped to a resource for per-language overrides). It carries the same
// guard, so it must gate on the provider setting too.
it('isCompletionsEnabledWithTextResourceConfig is off when the Copilot provider is disabled', () => {
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration(completionsSetting, { '*': true });
configurationService.setUserConfiguration(providerSetting, false);

expect(isCompletionsEnabledWithTextResourceConfig(textResourceConfig(configurationService), resource)).toBe(false);
});

it('isCompletionsEnabledWithTextResourceConfig is on when the Copilot provider is unset', () => {
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration(completionsSetting, { '*': true });

expect(isCompletionsEnabledWithTextResourceConfig(textResourceConfig(configurationService), resource)).toBe(true);
});
});
3 changes: 3 additions & 0 deletions src/vs/sessions/test/web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ class MockChatEntitlementService implements IChatEntitlementService {

readonly sentiment: IChatSentiment = { completed: true, registered: true };
readonly sentimentObs: IObservable<IChatSentiment> = observableValue('sentiment', { completed: true, registered: true });
// --- Start Positron ---
readonly isCompletionsOnlyMode = false;
// --- End Positron ---

readonly anonymous = false;
readonly anonymousObs: IObservable<boolean> = observableValue('anonymous', false);
Expand Down
Loading
Loading