diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts index e5a1abb3cc..9cbc18c074 100644 --- a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -468,4 +468,55 @@ declare module 'vscode' { */ readonly fullReferenceName?: string; } + + // #region Quota Sync + + /** + * A snapshot of quota usage for a single category (chat, completions, premium chat). + */ + export interface ChatQuotaSnapshot { + readonly percentRemaining: number; + readonly unlimited: boolean; + readonly hasQuota?: boolean; + readonly resetAt?: number; + readonly usageBasedBilling?: boolean; + readonly entitlement?: number; + readonly quotaRemaining?: number; + } + + /** + * A snapshot of rate limit usage for a category (session or weekly). + */ + export interface ChatRateLimitSnapshot { + readonly percentRemaining: number; + readonly unlimited: boolean; + readonly resetDate?: string; + } + + /** + * Quota snapshot data covering all categories. + * Accepted by {@link chat.updateQuotas} for extension-to-core sync. + */ + export interface ChatQuotaSnapshots { + readonly resetDate?: string; + readonly resetDateHasTime?: boolean; + readonly usageBasedBilling?: boolean; + readonly canUpgradePlan?: boolean; + readonly chat?: ChatQuotaSnapshot; + readonly completions?: ChatQuotaSnapshot; + readonly premiumChat?: ChatQuotaSnapshot; + readonly additionalUsageEnabled?: boolean; + readonly additionalUsageCount?: number; + readonly sessionRateLimit?: ChatRateLimitSnapshot; + readonly weeklyRateLimit?: ChatRateLimitSnapshot; + } + + export namespace chat { + /** + * Push quota snapshot data from the extension to the core workbench. + */ + export function updateQuotas(quotas: ChatQuotaSnapshots): void; + } + + // #endregion } diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index b46ec3015d..386bea6d01 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -336,6 +336,26 @@ declare module 'vscode' { */ archived?: boolean; + /** + * Resource identifier this item was previously known by. When set, host-stored + * per-resource state (archive, pin, read) recorded under that URI is treated as + * also applying to this item. + * + * On first access of state for {@link resource}, the host adopts the entry + * stored under `legacyResource` forward - copying it onto {@link resource} and + * removing the legacy entry. The migration is transparent: no events fire and + * the effective user-visible state is unchanged. + * + * Intended for providers that need to change the URI shape they emit (e.g. during + * a backend or schema migration) without requiring users to re-archive or re-pin + * items. + * + * The legacy URI's scheme must match {@link resource}'s scheme; otherwise the + * field is ignored. Multi-hop migrations are not supported - providers should + * collapse intermediate hops on their side and emit the original URI. + */ + readonly legacyResource?: Uri; + /** * Timing information for the chat session */ @@ -658,6 +678,48 @@ declare module 'vscode' { * unique across the provider's groups; on conflict, the first declared wins. */ readonly slashCommand?: string; + + /** + * Optional tooltip content shown in a hover panel when the user focuses or + * hovers over this item in the picker. Supports markdown formatting. + */ + readonly tooltip?: string; + + /** + * Optional model metadata for this option item. When present, the picker + * renders a rich hover with model name, pricing, context size, and capabilities + * instead of a plain text tooltip. + */ + readonly modelMetadata?: ChatSessionProviderOptionModelMetadata; + } + + /** + * Metadata describing a language model, used to render rich hover content + * in option group pickers. Fields mirror {@link LanguageModelChatInformation} + * so the core can reuse its standard model hover rendering. + */ + export interface ChatSessionProviderOptionModelMetadata { + readonly name: string; + readonly id: string; + readonly vendor?: string; + readonly version?: string; + readonly family?: string; + readonly tooltip?: string; + readonly pricing?: string; + readonly multiplierNumeric?: number; + readonly inputCost?: number; + readonly outputCost?: number; + readonly cacheCost?: number; + readonly longContextInputCost?: number; + readonly longContextOutputCost?: number; + readonly longContextCacheCost?: number; + readonly priceCategory?: string; + readonly maxInputTokens?: number; + readonly maxOutputTokens?: number; + readonly capabilities?: { + readonly vision?: boolean; + readonly toolCalling?: boolean; + }; } /** diff --git a/src/github/pullRequestGitHelper.ts b/src/github/pullRequestGitHelper.ts index 0fe25edf0f..cb49963292 100644 --- a/src/github/pullRequestGitHelper.ts +++ b/src/github/pullRequestGitHelper.ts @@ -333,8 +333,16 @@ export class PullRequestGitHelper { try { const configKey = this.getMetadataKeyForBranch(branchName); const allConfigs = await repository.getConfigs(); - const matchingConfigs = allConfigs.filter(config => config.key === configKey).sort((a, b) => b.value < a.value ? 1 : -1); - return PullRequestGitHelper.parsePullRequestMetadata(matchingConfigs[0].value); + // When the same branch name has been associated with multiple PRs over + // time (resulting in duplicate config entries), prefer the most recent + // association: parse the trailing PR number and sort numerically so the + // highest PR number wins. Falls back to the first entry if no match. + const matchingConfigs = allConfigs + .filter(config => config.key === configKey) + .map(config => ({ config, metadata: PullRequestGitHelper.parsePullRequestMetadata(config.value) })) + .filter((entry): entry is { config: { key: string; value: string }, metadata: PullRequestMetadata } => !!entry.metadata) + .sort((a, b) => b.metadata.prNumber - a.metadata.prNumber); + return matchingConfigs[0]?.metadata; } catch (_) { return; } diff --git a/src/test/github/pullRequestGitHelper.test.ts b/src/test/github/pullRequestGitHelper.test.ts index 590b35f0aa..4a4a8b9f45 100644 --- a/src/test/github/pullRequestGitHelper.test.ts +++ b/src/test/github/pullRequestGitHelper.test.ts @@ -188,4 +188,27 @@ describe('PullRequestGitHelper', function () { assert.strictEqual(await repository.getConfig('branch.pr/me/100.github-pr-owner-number'), 'owner#name#100'); }); }); + + describe('getMatchingPullRequestMetadataForBranch', function () { + it('returns the highest-numbered PR when duplicate config entries exist for the branch', async function () { + // Simulate the case where a branch name has been associated with multiple + // PRs over time and `git config --get-all` returns duplicate entries. + // The helper should prefer the most recent association (highest PR + // number for the same owner/repo), not the lowest. + sinon.stub(repository, 'getConfigs').resolves([ + { key: 'branch.feature.github-pr-owner-number', value: 'owner#name#5' }, + { key: 'branch.feature.github-pr-owner-number', value: 'owner#name#42' }, + { key: 'branch.feature.github-pr-owner-number', value: 'owner#name#17' }, + { key: 'branch.other.github-pr-owner-number', value: 'owner#name#999' }, + ]); + + const metadata = await PullRequestGitHelper.getMatchingPullRequestMetadataForBranch(repository, 'feature'); + + assert.deepStrictEqual(metadata, { + owner: 'owner', + repositoryName: 'name', + prNumber: 42, + }); + }); + }); }); diff --git a/src/view/reviewManager.ts b/src/view/reviewManager.ts index 38bc2b3341..bc8d45db43 100644 --- a/src/view/reviewManager.ts +++ b/src/view/reviewManager.ts @@ -428,7 +428,17 @@ export class ReviewManager extends Disposable { private async getUpstreamUrlAndName(branch: Branch): Promise<{ remoteUrl: string | undefined, upstreamBranchName: string | undefined, remoteName: string | undefined }> { if (branch.upstream) { Logger.debug(`Upstream for branch ${branch.name} is ${branch.upstream.remote}/${branch.upstream.name}`, this.id); - return { remoteName: branch.upstream.remote, upstreamBranchName: branch.upstream.name, remoteUrl: undefined }; + // When a branch is created tracking a different remote branch (e.g. via + // `git worktree add -b feature-foo origin/develop`), git sets + // `branch..merge` to the upstream's branch (`develop`) even though + // the eventual PR head ref is the local branch name (`feature-foo`). + // Prefer the local branch name when it differs from the tracked upstream, + // since GitHub PR `headRefName` matches the pushed branch name, which + // defaults to the local branch name. + const upstreamBranchName = (branch.name && branch.upstream.name !== branch.name) + ? branch.name + : branch.upstream.name; + return { remoteName: branch.upstream.remote, upstreamBranchName, remoteUrl: undefined }; } else { try { const remoteUrl = await this.repository.getConfig(`branch.${branch.name}.remote`);