Skip to content
Draft
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
51 changes: 51 additions & 0 deletions src/@types/vscode.proposed.chatParticipantPrivate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
62 changes: 62 additions & 0 deletions src/@types/vscode.proposed.chatSessionsProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
};
}

/**
Expand Down
12 changes: 10 additions & 2 deletions src/github/pullRequestGitHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
23 changes: 23 additions & 0 deletions src/test/github/pullRequestGitHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
});
12 changes: 11 additions & 1 deletion src/view/reviewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> origin/develop`), git sets
// `branch.<name>.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`);
Expand Down