Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8d96288
Add marketplace ref support for plugin marketplaces
aaronpowell May 22, 2026
30d40fe
Apply suggestions from code review
aaronpowell May 22, 2026
881d7cd
agentHost: adopt instructionDirectories for syncing custom instructions
connor4312 May 22, 2026
73bc959
agentHost: sync user customizations, fix fs not restoring on reconnect
connor4312 May 22, 2026
72b76ef
agentHost: introduce IAgentHostActiveClientService
connor4312 May 22, 2026
0c1f555
test(agentHost): fix dirname grant expectation in customization impli…
connor4312 May 22, 2026
7ce8f58
address Copilot review comments
connor4312 May 22, 2026
2981c93
Removing unused method
aaronpowell May 23, 2026
3b36773
Disable walkthrough if ai features disabled
cwebster-99 May 26, 2026
65a326a
Not opening secondary sidebar when ai features disabled
cwebster-99 May 26, 2026
b62940a
Using sentiment hidden for startup page
cwebster-99 May 26, 2026
858860a
fix test
connor4312 May 26, 2026
0d45577
Merge remote-tracking branch 'origin/main' into connor4312/ah-instruc…
connor4312 May 26, 2026
4dd95a0
build(deps-dev): bump qs from 6.14.2 to 6.15.2 in /build (#318140)
dependabot[bot] May 26, 2026
ceba492
build(deps): bump qs from 6.14.2 to 6.15.2 in /extensions/copilot (#3…
dependabot[bot] May 26, 2026
ad5a6f2
Merge pull request #318051 from microsoft/connor4312/ah-instructions
connor4312 May 26, 2026
ce732e4
Merge pull request #317901 from aaronpowell/aaronpowell/plugin-market…
connor4312 May 26, 2026
b07c6b3
build(deps): bump @nevware21/ts-utils from 0.11.6 to 0.14.0 in /exten…
dependabot[bot] May 26, 2026
823ab42
build(deps): bump @nevware21/ts-utils from 0.11.6 to 0.14.0 in /exten…
dependabot[bot] May 26, 2026
a058a50
build(deps): bump @nevware21/ts-utils from 0.11.6 to 0.14.0 in /exten…
dependabot[bot] May 26, 2026
7d2010e
build(deps): bump @nevware21/ts-utils from 0.11.6 to 0.14.0 in /exten…
dependabot[bot] May 26, 2026
c2895b5
Merge pull request #318364 from microsoft/occupational-crocodile
cwebster-99 May 26, 2026
aeba1b9
Reapply "Fix UBB model details in Copilot CLI (#316452)" (#316698) (#…
pwang347 May 26, 2026
c89290e
Only update sandbox config on connect and user settings change (#318338)
chrmarti May 26, 2026
796230d
Do not show multipliers for cloud model picker for UBB accounts (#318…
pwang347 May 26, 2026
10dd579
Change icon for Toggle Developer Tools action (#318431)
jruales May 26, 2026
0e3051a
Fix: cross-file inline edit list never disposed (handleListEndOfLifet…
ulugbekna May 26, 2026
8bfc82f
sessions improvements
benibenj May 26, 2026
359e80c
Merge pull request #318435 from microsoft/benibenj/satisfied-pig
benibenj May 26, 2026
5f3991a
Agents web: surface offline tunnels in host picker + add discovery te…
osortega May 26, 2026
4b04bed
agent host: clear sticky incompatible status after upgrade reconnect …
connor4312 May 26, 2026
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
6 changes: 3 additions & 3 deletions build/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions extensions/copilot/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export interface CopilotCLIModelInfo {
readonly id: string;
readonly name: string;
readonly multiplier?: number;
readonly priceCategory?: string;
readonly inputCost?: number;
readonly outputCost?: number;
readonly cacheCost?: number;
readonly maxInputTokens?: number;
readonly maxOutputTokens?: number;
readonly maxContextWindowTokens: number;
Expand Down Expand Up @@ -152,18 +156,26 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
const [{ getAvailableModels }, authInfo] = await Promise.all([this.copilotCLISDK.getPackage(), this.copilotCLISDK.getAuthInfo()]);
try {
const models = await getAvailableModels(authInfo);
return models.map(model => ({
id: model.id,
name: model.name,
multiplier: model.billing?.multiplier,
maxInputTokens: model.capabilities.limits.max_prompt_tokens,
maxOutputTokens: model.capabilities.limits.max_output_tokens,
maxContextWindowTokens: model.capabilities.limits.max_context_window_tokens,
supportsVision: model.capabilities.supports.vision,
supportsReasoningEffort: model.capabilities.supports.reasoningEffort,
defaultReasoningEffort: model.defaultReasoningEffort,
supportedReasoningEfforts: model.supportedReasoningEfforts,
} satisfies CopilotCLIModelInfo));
return models.map(model => {
const tokenPrices = model.billing?.token_prices;
const normalizedPricing = normalizeTokenPricing(tokenPrices);
return {
id: model.id,
name: model.name,
multiplier: model.billing?.multiplier,
priceCategory: model.model_picker_price_category,
inputCost: normalizedPricing?.inputPrice,
outputCost: normalizedPricing?.outputPrice,
cacheCost: normalizedPricing?.cachePrice,
maxInputTokens: model.capabilities.limits.max_prompt_tokens,
maxOutputTokens: model.capabilities.limits.max_output_tokens,
maxContextWindowTokens: model.capabilities.limits.max_context_window_tokens,
supportsVision: model.capabilities.supports.vision,
supportsReasoningEffort: model.capabilities.supports.reasoningEffort,
defaultReasoningEffort: model.defaultReasoningEffort,
supportedReasoningEfforts: model.supportedReasoningEfforts,
} satisfies CopilotCLIModelInfo;
});
} catch (ex) {
this.logService.error(`[CopilotCLISession] Failed to fetch models`, ex);
// Clear cached promise so subsequent calls retry instead of
Expand Down Expand Up @@ -208,6 +220,10 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
maxInputTokens: model.maxInputTokens ?? model.maxContextWindowTokens,
maxOutputTokens: model.maxOutputTokens ?? 0,
pricing: multiplier,
priceCategory: model.priceCategory,
inputCost: model.inputCost,
outputCost: model.outputCost,
cacheCost: model.cacheCost,
multiplierNumeric: model.multiplier,
isUserSelectable: true,
configurationSchema: isReasoningEffortEnabled ? buildConfigurationSchema(model) : undefined,
Expand Down Expand Up @@ -613,3 +629,24 @@ export function isEnabledForCopilotCLI(customization: { sessionTypes?: readonly
return sessionTypes === undefined || sessionTypes.includes('copilotcli') || false;
}

const AIC_DIVISOR = 1_000_000_000;
const TOKENS_PER_MILLION = 1_000_000;

/**
* Converts raw billing token prices (nano-AICs with a batch_size) into
* normalized AICs per million tokens, matching the normalization in
* chatEndpoint.ts for non-CLI models.
*/
function normalizeTokenPricing(tokenPrices: { input_price?: number; output_price?: number; cache_price?: number; batch_size?: number } | undefined): { inputPrice: number; outputPrice: number; cachePrice: number | undefined } | undefined {
if (!tokenPrices || tokenPrices.input_price === undefined || tokenPrices.output_price === undefined) {
return undefined;
}
const batchSize = tokenPrices.batch_size ?? TOKENS_PER_MILLION;
const scale = TOKENS_PER_MILLION / batchSize;
return {
inputPrice: (tokenPrices.input_price / AIC_DIVISOR) * scale,
outputPrice: (tokenPrices.output_price / AIC_DIVISOR) * scale,
cachePrice: tokenPrices.cache_price !== undefined ? (tokenPrices.cache_price / AIC_DIVISOR) * scale : undefined,
};
}

Original file line number Diff line number Diff line change
Expand Up @@ -995,10 +995,11 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
}

if (models.status === 'fulfilled' && models.value.length > 0) {
const isUBB = !!this._authenticationService.copilotToken?.isUsageBasedBilling;
const modelItems: vscode.ChatSessionProviderOptionItem[] = models.value.map(model => ({
id: model.id,
name: model.name,
...(model.billing?.multiplier !== undefined ? { description: `${model.billing.multiplier}x` } : {}),
...(!isUBB && model.billing?.multiplier !== undefined ? { description: `${model.billing.multiplier}x` } : {}),
}));
if (!models.value.find(m => m.id === DEFAULT_MODEL_ID)) {
modelItems.unshift({ id: DEFAULT_MODEL_ID, name: vscode.l10n.t('Auto'), description: vscode.l10n.t('Automatically select the best model') });
Expand Down
16 changes: 13 additions & 3 deletions extensions/git/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions extensions/github/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions extensions/json-language-features/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions extensions/typescript-language-features/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -1203,7 +1203,8 @@ export class InlineCompletionsModel extends Disposable {
* Used for cross-file inline edits.
*/
public transplantCompletion(item: InlineSuggestionItem): void {
item.addRef();
// No explicit addRef needed: `seedWithCompletion` creates a new `InlineCompletionsState`
// which calls `addRef` on every item it holds and pairs it with `removeRef` in dispose.
transaction(tx => {
this._source.seedWithCompletion(item, tx);
this._isActive.set(true, tx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,32 @@

import { assertNever } from '../../../../../base/common/assert.js';
import { AsyncIterableProducer } from '../../../../../base/common/async.js';
import { CachedFunction } from '../../../../../base/common/cache.js';
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
import { BugIndicatingError, onUnexpectedExternalError } from '../../../../../base/common/errors.js';
import { groupByMap } from '../../../../../base/common/collections.js';
import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from '../../../../../base/common/errors.js';
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
import { isDefined } from '../../../../../base/common/types.js';
import { URI } from '../../../../../base/common/uri.js';
import { prefixedUuid } from '../../../../../base/common/uuid.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { ISingleEditOperation } from '../../../../common/core/editOperation.js';
import { StringReplacement } from '../../../../common/core/edits/stringEdit.js';
import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';
import { TextReplacement } from '../../../../common/core/edits/textEdit.js';
import { Position } from '../../../../common/core/position.js';
import { Range } from '../../../../common/core/range.js';
import { TextReplacement } from '../../../../common/core/edits/textEdit.js';
import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, PartialAcceptInfo, InlineCompletionsDisposeReason, LifetimeSummary, ProviderId, IInlineCompletionHint, InlineCompletionTriggerKind } from '../../../../common/languages.js';
import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';
import { IInlineCompletionHint, InlineCompletion, InlineCompletionContext, InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletions, InlineCompletionsDisposeReason, InlineCompletionsProvider, InlineCompletionTriggerKind, LifetimeSummary, PartialAcceptInfo, ProviderId } from '../../../../common/languages.js';
import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';
import { ITextModel } from '../../../../common/model.js';
import { fixBracketsInLine } from '../../../../common/model/bracketPairsTextModelPart/fixBrackets.js';
import { EditDeltaInfo } from '../../../../common/textModelEditSource.js';
import { SnippetParser, Text } from '../../../snippet/browser/snippetParser.js';
import { ErrorResult, getReadonlyEmptyArray } from '../utils.js';
import { groupByMap } from '../../../../../base/common/collections.js';
import { DirectedGraph } from './graph.js';
import { CachedFunction } from '../../../../../base/common/cache.js';
import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js';
import { isDefined } from '../../../../../base/common/types.js';
import { inlineCompletionIsVisible } from './inlineCompletionIsVisible.js';
import { EditDeltaInfo } from '../../../../common/textModelEditSource.js';
import { URI } from '../../../../../base/common/uri.js';
import { InlineSuggestionEditKind } from './editKind.js';
import { DirectedGraph } from './graph.js';
import { inlineCompletionIsVisible } from './inlineCompletionIsVisible.js';
import { InlineSuggestAlternativeAction } from './InlineSuggestAlternativeAction.js';

export type InlineCompletionContextWithoutUuid = Omit<InlineCompletionContext, 'requestUuid'>;
Expand Down Expand Up @@ -659,6 +659,12 @@ export class InlineSuggestionList {
item.reportEndOfLife();
}
this.provider.disposeInlineCompletions(this.inlineSuggestions, reason);
} else if (this.refCount < 0) {
// Invariant: every addRef must be paired with exactly one removeRef.
// Going negative means a removeRef without a matching addRef somewhere.
onUnexpectedError(new BugIndicatingError(
`InlineSuggestionList (provider=${this.provider.providerId?.toString()}) refCount went negative (${this.refCount}) — more removeRef than addRef calls.`
));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { AgentHostTelemetryLevelConfigKey, AgentHostSessionSyncEnabledConfigKey,
import type { OtlpExportLogsParams } from '../common/state/protocol/channels-otlp/notifications.js';
import type { TelemetryCapabilities } from '../common/state/protocol/channels-otlp/state.js';
import type { InitializeResult } from '../common/state/protocol/common/commands.js';
import { dirname } from '../../../base/common/resources.js';

const AHP_CLIENT_CONNECTION_CLOSED = -32000;

Expand Down Expand Up @@ -880,14 +881,15 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
} catch {
continue;
}
const key = uri.toString();
const grantUri = dirname(uri);
const key = grantUri.toString();
if (this._grantedCustomizationUris.has(key)) {
continue;
}
this._grantedCustomizationUris.add(key);
// Disposable is owned by the permission service; cleared on
// connectionClosed.
this._permissionService.grantImplicitRead(this._address, uri);
this._permissionService.grantImplicitRead(this._address, grantUri);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/vs/platform/agentHost/node/copilot/copilotAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { IAgentHostCheckpointService } from '../../common/agentHostCheckpointSer
import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
import { CopilotAgentSession, SessionWrapperFactory, type CopilotSdkMode, type IActiveClientSnapshot } from './copilotAgentSession.js';
import { ICopilotSessionContext, projectFromCopilotContext } from './copilotGitProject.js';
import { parsedPluginsEqual, toCustomizationAgentRefs, toSdkCustomAgents, toSdkHooks, toSdkMcpServers, toSdkSkillDirectories } from './copilotPluginConverters.js';
import { parsedPluginsEqual, toCustomizationAgentRefs, toSdkCustomAgents, toSdkHooks, toSdkInstructionDirectories, toSdkMcpServers, toSdkSkillDirectories } from './copilotPluginConverters.js';
import { CopilotSessionWrapper } from './copilotSessionWrapper.js';
import { ShellManager, createShellTools } from './copilotShellTools.js';
import { SessionCustomizationDiscovery } from './sessionCustomizationDiscovery.js';
Expand Down Expand Up @@ -1504,6 +1504,7 @@ export class CopilotAgent extends Disposable implements IAgent {
mcpServers: toSdkMcpServers(plugins.flatMap(p => p.mcpServers)),
customAgents,
skillDirectories: toSdkSkillDirectories(plugins.flatMap(p => p.skills)),
instructionDirectories: toSdkInstructionDirectories(plugins.flatMap(p => p.instructions)),
systemMessage: COPILOT_AGENT_HOST_SYSTEM_MESSAGE,
tools: [...shellTools, ...callbacks.clientTools],
// Enable infinite sessions so the SDK provisions a workspace
Expand Down
18 changes: 15 additions & 3 deletions src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,22 @@ export function toCustomizationAgentRefs(agents: readonly INamedPluginResource[]
* The SDK expects directory paths; we extract the parent directory of each SKILL.md.
*/
export function toSdkSkillDirectories(skills: readonly INamedPluginResource[]): string[] {
return toSdkResourceDirectories(skills);
}

/**
* Converts parsed plugin instructions into the SDK's
* `instructionDirectories` config.
*/
export function toSdkInstructionDirectories(instructions: readonly INamedPluginResource[]): string[] {
return toSdkResourceDirectories(instructions);
}

function toSdkResourceDirectories(resources: readonly INamedPluginResource[]): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const skill of skills) {
// SKILL.md parent directory is the skill directory
const dir = dirname(skill.uri.fsPath);
for (const resource of resources) {
const dir = dirname(resource.uri.fsPath);
if (!seen.has(dir)) {
seen.add(dir);
result.push(dir);
Expand Down Expand Up @@ -373,6 +384,7 @@ export function parsedPluginsEqual(a: readonly IParsedPlugin[], b: readonly IPar
mcpServers: p.mcpServers.map(m => ({ name: m.name, configuration: m.configuration })),
skills: p.skills.map(s => ({ uri: s.uri.toString(), name: s.name })),
agents: p.agents.map(a => ({ uri: a.uri.toString(), name: a.name })),
instructions: p.instructions.map(i => ({ uri: i.uri.toString(), name: i.name })),
})));
};
return serialize(a) === serialize(b);
Expand Down
Loading
Loading