Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
611bf74
markdown: fix scroll sync regressions introduced in #287050
AshtonYoon Apr 4, 2026
4e9dd03
Merge branch 'main' into fix/markdown-preview-scroll-sync-regressions
mjbvz Apr 4, 2026
ffa3fec
Disable emmet commands when there is no active editor
mjbvz May 1, 2026
dd112d4
Add usage hover and style tweaks (#313743)
pwang347 May 1, 2026
0b758f4
Make chat input notification text always inline (#313768)
pwang347 May 1, 2026
c1df4d9
Remove agentHistorySummarizationInline experiment (#313757)
bhavyaus May 1, 2026
c25d705
Refactor IChatModes to support session-based modes (#313765)
aeschli May 1, 2026
1585dda
Add telemetry for ubb (#313772)
pwang347 May 1, 2026
f97ad82
Merge pull request #313646 from microsoft/dev/mjbvz/alternative-lamprey
mjbvz May 1, 2026
d878141
Add reindex cmd pallete (#313776)
vijayupadya May 1, 2026
53964c4
Get model and multiplier show up for Copilot CLI controller API route…
anthonykim1 May 1, 2026
cfa5454
Run agent host shell tools on independent terminals (#313789)
connor4312 May 1, 2026
7bf921d
Yemohyle/add to ext telemetrey (#313159)
yemohyleyemohyle May 1, 2026
7211c0f
agentHost/claude: Phase 3 reference grounding + Phase 4 ClaudeAgent s…
TylerLeonhardt May 1, 2026
d3e91ab
Move agent host picker to sessions sidebar (web desktop) (#313619)
osortega May 1, 2026
1d94ae1
gate claude model behind setting too (#313801)
justschen May 1, 2026
ff66ce6
Merge branch 'main' into fix/markdown-preview-scroll-sync-regressions
mjbvz May 1, 2026
7f53a83
Merge pull request #307763 from AshtonYoon/fix/markdown-preview-scrol…
mjbvz May 1, 2026
682c790
Don't dismiss hover when mouse exits due to programmatic resize (#313…
pwang347 May 1, 2026
8d9a4bb
Add support for UBB in manage models view (#313741)
pwang347 May 1, 2026
e6b9ae7
AHP/Customizations: BrowserPluginGitCommandService for adding plugins…
joshspicer May 1, 2026
675ef9f
[cli] reduce info logs (#313822)
rebornix May 2, 2026
9b92388
UseChatSessionCustomizationsForCustomAgents setting (#313781)
aeschli May 2, 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
24 changes: 14 additions & 10 deletions extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2684,6 +2684,12 @@
"category": "Chat",
"enablement": "github.copilot.sessionSearch.enabled && config.chat.sessionSync.enabled"
},
{
"command": "github.copilot.chronicle.reindex",
"title": "%github.copilot.command.chronicle.reindex%",
"category": "Chat",
"enablement": "github.copilot.sessionSearch.enabled"
},
{
"command": "github.copilot.nes.captureExpected.start",
"title": "Record Expected Edit (NES)",
Expand Down Expand Up @@ -4499,16 +4505,6 @@
"experimental"
]
},
"github.copilot.chat.agentHistorySummarizationInline": {
"type": "boolean",
"default": true,
"markdownDescription": "%github.copilot.config.agentHistorySummarizationInline%",
"tags": [
"advanced",
"experimental",
"onExp"
]
},
"github.copilot.chat.useResponsesApiTruncation": {
"type": "boolean",
"default": false,
Expand Down Expand Up @@ -4678,6 +4674,14 @@
"advanced"
]
},
"github.copilot.chat.agent.modelDetails.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "%github.copilot.config.chat.agent.modelDetails.enabled%",
"tags": [
"advanced"
]
},
"github.copilot.chat.cli.planCommand.enabled": {
"type": "boolean",
"default": true,
Expand Down
3 changes: 2 additions & 1 deletion extensions/copilot/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
"copilot.chronicle.standup.description": "Generate a standup report from recent chat sessions",
"copilot.chronicle.tips.description": "Get personalized tips based on your chat session usage patterns",
"github.copilot.command.sessionSync.deleteSessions": "Delete Session Sync Data",
"github.copilot.command.chronicle.reindex": "Reindex Sessions",
"copilot.chronicle.reindex.description": "Rebuild the local session index from stored session logs. Add 'force' to re-process already indexed sessions.",
"github.copilot.config.sessionSearch.enabled": "Enable session search and /chronicle commands. This is a team-internal setting.",
"github.copilot.config.sessionSearch.localIndex.enabled": "Enable local session tracking. When enabled, Copilot tracks session data locally for /chronicle commands.",
Expand Down Expand Up @@ -398,7 +399,6 @@
"github.copilot.config.instantApply.shortContextLimit": "Token limit for short context instant apply.",
"github.copilot.config.summarizeAgentConversationHistoryThreshold": "Threshold for compacting agent conversation history.",
"github.copilot.config.agentHistorySummarizationMode": "Mode for agent history summarization.",
"github.copilot.config.agentHistorySummarizationInline": "Summarize conversation inline within the agent loop instead of a separate LLM call, maximizing prompt cache hits.",
"github.copilot.config.useResponsesApiTruncation": "Use Responses API for truncation.",
"github.copilot.config.enableReadFileV2": "Enable version 2 of the read file tool.",
"github.copilot.config.enableAskAgent": "Enable the Ask agent for answering questions.",
Expand All @@ -413,6 +413,7 @@
"github.copilot.config.cli.showExternalSessions": "Show sessions created by other applications.",
"github.copilot.config.cli.planExitMode.enabled": "Enable Plan Mode exit handling in Copilot CLI.",
"github.copilot.config.cli.autoModel.enabled": "Enable the Auto model option in Copilot CLI, which automatically selects the best model for each request. Requires VS Code reload.",
"github.copilot.config.chat.agent.modelDetails.enabled": "Show model details (model name and request multiplier) on agent chat responses when using Copilot CLI or Claude agent in VS Code. Requires VS Code reload to update already loaded sessions.",
"github.copilot.config.cli.planCommand.enabled": "Enable the /plan command in Copilot CLI to create implementation plans before coding.",
"github.copilot.config.cli.lazyLoadSessionItem.enabled": "Enable lazy loading of session items in Copilot CLI. Requires VS Code reload.",
"github.copilot.config.cli.aiGenerateBranchNames.enabled": "Enable AI-generated branch names in Copilot CLI.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ export class ClaudeCodeModels extends Disposable implements IClaudeCodeModels {
maxInputTokens: endpoint.modelMaxPromptTokens,
maxOutputTokens: endpoint.maxOutputTokens,
pricing: multiplier ?? (endpoint.tokenPricing ? formatPricingLabel(endpoint.tokenPricing) : undefined),
inputCost: endpoint.tokenPricing?.inputPrice,
outputCost: endpoint.tokenPricing?.outputPrice,
cacheCost: endpoint.tokenPricing?.cacheReadTokenPrice,
multiplierNumeric: endpoint.multiplier,
tooltip,
isUserSelectable: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ export interface RequestDetails {
/** Mode instructions for this request (excluding toolReferences). */
modeInstructions?: StoredModeInstructions;

/**
* The concrete model id that produced the response for this request, as reported by the
* SDK's `assistant.usage` event. Captured so that on session reload we can render the
* correct model details (e.g. for `auto`, where the resolved model is not otherwise
* recoverable from the persisted SDK event log).
*/
responseModelId?: string;

/** Checkpoint reference for this request (primary workspace). */
checkpointRef?: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,22 +518,57 @@ export interface RequestIdDetails {
readonly requestId: string;
readonly toolIdEditMap: Record<string, string>;
readonly modeInstructions?: StoredModeInstructions;
readonly responseModelId?: string;
}

/**
* Build chat history from SDK events for VS Code chat session
* Converts SDKEvents into ChatRequestTurn2 and ChatResponseTurn2 objects
*/
export function buildChatHistoryFromEvents(sessionId: string, modelId: string | undefined, events: readonly SessionEvent[], getVSCodeRequestId: (sdkRequestId: string) => RequestIdDetails | undefined, delegationSummaryService: IChatDelegationSummaryService, logger: ILogger, workingDirectory?: URI, defaultModeInstructionsForLastRequest?: StoredModeInstructions, lastResponseDetails?: string): (ChatRequestTurn2 | ChatResponseTurn2)[] {
export function buildChatHistoryFromEvents(sessionId: string, modelId: string | undefined, events: readonly SessionEvent[], getVSCodeRequestId: (sdkRequestId: string) => RequestIdDetails | undefined, delegationSummaryService: IChatDelegationSummaryService, logger: ILogger, workingDirectory?: URI, defaultModeInstructionsForLastRequest?: StoredModeInstructions, modelDetailsById?: ReadonlyMap<string, string>): (ChatRequestTurn2 | ChatResponseTurn2)[] {
const turns: (ChatRequestTurn2 | ChatResponseTurn2)[] = [];
let currentResponseParts: ExtendedChatResponsePart[] = [];
const pendingToolInvocations = new Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>();

let details: RequestIdDetails | undefined;
let isFirstUserMessage = true;
let currentModelId = modelId;
let currentResponseModelId: string | undefined;
let currentRequestTurnIndex: number | undefined;
const currentAssistantMessage: { chunks: string[] } = { chunks: [] };
const processedMessages = new Set<string>();

function getModelDetails(modelId: string | undefined): string | undefined {
if (!modelId || !modelDetailsById) {
return undefined;
}
return modelDetailsById.get(modelId.trim().toLowerCase());
}

function createResultForModel(modelId: string | undefined) {
const details = getModelDetails(modelId);
return details ? { details } : {};
}

function flushResponseParts() {
if (currentResponseParts.length > 0) {
turns.push(new ChatResponseTurn2(currentResponseParts, createResultForModel(currentResponseModelId ?? currentModelId), ''));
currentResponseParts = [];
}
currentResponseModelId = undefined;
currentRequestTurnIndex = undefined;
}

function updateCurrentRequestModelId(modelId: string | undefined) {
if (currentRequestTurnIndex === undefined || !modelId) {
return;
}
const turn = turns[currentRequestTurnIndex];
if (turn instanceof ChatRequestTurn2 && turn.modelId !== modelId) {
turns[currentRequestTurnIndex] = new ChatRequestTurn2(turn.prompt, turn.command, turn.references, turn.participant, [...turn.toolReferences], turn.editedFileEvents, turn.id, modelId, turn.modeInstructions2);
}
}

function processAssistantMessage(content: string) {
// Extract PR metadata if present
const { cleanedContent, prPart } = extractPRMetadata(content);
Expand Down Expand Up @@ -562,16 +597,33 @@ export function buildChatHistoryFromEvents(sessionId: string, modelId: string |
}

switch (event.type) {
case 'session.start':
case 'session.resume': {
currentModelId = event.data.selectedModel ?? currentModelId;
break;
}
case 'session.model_change': {
currentModelId = event.data.newModel;
if (currentRequestTurnIndex !== undefined && currentResponseParts.length === 0) {
currentResponseModelId = currentModelId;
updateCurrentRequestModelId(currentModelId);
}
break;
}
case 'assistant.usage': {
currentModelId = event.data.model ?? currentModelId;
if (currentRequestTurnIndex !== undefined) {
currentResponseModelId = currentModelId;
updateCurrentRequestModelId(currentModelId);
}
break;
}
case 'user.message': {
if (isSyntheticUserMessage(event)) {
continue;
}
details = getVSCodeRequestId(event.id);
// Flush any pending response parts before adding user message
if (currentResponseParts.length > 0) {
turns.push(new ChatResponseTurn2(currentResponseParts, {}, ''));
currentResponseParts = [];
}
flushResponseParts();
// Filter out vscode instruction files from references when building session history
// TODO@rebornix filter instructions should be rendered as "references" in chat response like normal chat.
const references: ChatPromptReference[] = [];
Expand Down Expand Up @@ -675,7 +727,13 @@ export function buildChatHistoryFromEvents(sessionId: string, modelId: string |
}
}

turns.push(new ChatRequestTurn2(`${commandPrefix}${prompt}`, undefined, references, '', [], undefined, details?.requestId ?? event.id, modelId, modeInstructions2));
// Prefer the persisted resolved model id (from `assistant.usage`) so that on reload
// `auto` sessions show the actual model used to produce the response. Falls back to
// the currently tracked model id (from `session.start`/`session.model_change`).
const resolvedRequestModelId = details?.responseModelId ?? currentModelId;
currentResponseModelId = resolvedRequestModelId;
turns.push(new ChatRequestTurn2(`${commandPrefix}${prompt}`, undefined, references, '', [], undefined, details?.requestId ?? event.id, resolvedRequestModelId, modeInstructions2));
currentRequestTurnIndex = turns.length - 1;
break;
}
case 'assistant.message_delta': {
Expand Down Expand Up @@ -745,10 +803,7 @@ export function buildChatHistoryFromEvents(sessionId: string, modelId: string |
}

flushPendingAssistantMessage();

if (currentResponseParts.length > 0) {
turns.push(new ChatResponseTurn2(currentResponseParts, lastResponseDetails ? { details: lastResponseDetails } : {}, ''));
}
flushResponseParts();

return turns;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,92 @@ describe('CopilotCLITools', () => {
{ type: 'user.message', data: { content: 'Hello', attachments: [] } },
{ type: 'assistant.message', data: { content: 'Hi there' } }
];
const turns = buildChatHistoryFromEvents('', 'base', events, getVSCodeRequestId, delegationSummary, logger, undefined, undefined, 'Base • 2x');
const turns = buildChatHistoryFromEvents('', 'base', events, getVSCodeRequestId, delegationSummary, logger, undefined, undefined, new Map([['base', 'Base • 2x']]));
expect(turns).toHaveLength(2);
const responseTurn = turns[1] as ChatResponseTurn2;
expect(responseTurn.result).toEqual({ details: 'Base • 2x' });
});

it('uses session model changes for each rebuilt response turn', () => {
const modelDetails = new Map([
['opus-4.6', 'Opus 4.6 • 4x'],
['opus-4.7', 'Opus 4.7 • 4x'],
['gpt-5.4', 'GPT 5.4 • 2x'],
['gpt-5.3', 'GPT 5.3 • 1x'],
]);
const events: any[] = [
{ type: 'session.start', data: { selectedModel: 'opus-4.6' } },
{ type: 'user.message', id: 'u1', data: { content: 'First', attachments: [] } },
{ type: 'assistant.message', data: { content: 'One' } },
{ type: 'session.model_change', data: { previousModel: 'opus-4.6', newModel: 'opus-4.7' } },
{ type: 'user.message', id: 'u2', data: { content: 'Second', attachments: [] } },
{ type: 'assistant.message', data: { content: 'Two' } },
{ type: 'session.model_change', data: { previousModel: 'opus-4.7', newModel: 'gpt-5.4' } },
{ type: 'user.message', id: 'u3', data: { content: 'Third', attachments: [] } },
{ type: 'assistant.message', data: { content: 'Three' } },
{ type: 'session.model_change', data: { previousModel: 'gpt-5.4', newModel: 'gpt-5.3' } },
{ type: 'user.message', id: 'u4', data: { content: 'Fourth', attachments: [] } },
{ type: 'assistant.message', data: { content: 'Four' } },
];

const turns = buildChatHistoryFromEvents('', undefined, events, getVSCodeRequestId, delegationSummary, logger, undefined, undefined, modelDetails);

expect(turns.filter(turn => turn instanceof ChatRequestTurn2).map(turn => (turn as ChatRequestTurn2).modelId)).toEqual(['opus-4.6', 'opus-4.7', 'gpt-5.4', 'gpt-5.3']);
expect(turns.filter(turn => turn instanceof ChatResponseTurn2).map(turn => (turn as ChatResponseTurn2).result)).toEqual([
{ details: 'Opus 4.6 • 4x' },
{ details: 'Opus 4.7 • 4x' },
{ details: 'GPT 5.4 • 2x' },
{ details: 'GPT 5.3 • 1x' },
]);
});

it('uses assistant usage model for the active rebuilt response turn', () => {
const events: any[] = [
{ type: 'user.message', id: 'u1', data: { content: 'Hello', attachments: [] } },
{ type: 'assistant.message', data: { content: 'Hi' } },
{ type: 'assistant.usage', data: { model: 'gpt-5.4', inputTokens: 10, outputTokens: 5 } },
];

const turns = buildChatHistoryFromEvents('', undefined, events, getVSCodeRequestId, delegationSummary, logger, undefined, undefined, new Map([['gpt-5.4', 'GPT 5.4 • 2x']]));

expect(turns).toHaveLength(2);
expect((turns[0] as ChatRequestTurn2).modelId).toBe('gpt-5.4');
expect((turns[1] as ChatResponseTurn2).result).toEqual({ details: 'GPT 5.4 • 2x' });
});

it('uses persisted responseModelId to recover model details on reload for auto sessions', () => {
// Simulates a reloaded `auto` session: the SDK only persists `selectedModel: "auto"`
// (the `assistant.usage` event that carried the resolved model id is ephemeral and
// dropped from the persisted event log). The resolved model id was previously
// captured by the participant and stored via the chat session metadata store as
// `RequestDetails.responseModelId`, then surfaced through the `getVSCodeRequestId`
// callback. The reload path must use it to render the model footer details.
const events: any[] = [
{ type: 'session.start', data: { selectedModel: 'auto' } },
{ type: 'user.message', id: 'u1', data: { content: 'First', attachments: [] } },
{ type: 'assistant.message', data: { content: 'One' } },
{ type: 'user.message', id: 'u2', data: { content: 'Second', attachments: [] } },
{ type: 'assistant.message', data: { content: 'Two' } },
];
const detailsByEventId: Record<string, RequestIdDetails> = {
u1: { requestId: 'r1', toolIdEditMap: {}, responseModelId: 'gpt-5.4' },
u2: { requestId: 'r2', toolIdEditMap: {}, responseModelId: 'claude-opus-4.7' },
};
const lookup = (sdkRequestId: string) => detailsByEventId[sdkRequestId];

const turns = buildChatHistoryFromEvents('', 'auto', events, lookup, delegationSummary, logger, undefined, undefined, new Map([
['gpt-5.4', 'GPT 5.4 • 2x'],
['claude-opus-4.7', 'Claude Opus 4.7 • 4x'],
]));

expect(turns).toHaveLength(4);
expect(turns.filter(turn => turn instanceof ChatRequestTurn2).map(turn => (turn as ChatRequestTurn2).modelId)).toEqual(['gpt-5.4', 'claude-opus-4.7']);
expect(turns.filter(turn => turn instanceof ChatResponseTurn2).map(turn => (turn as ChatResponseTurn2).result)).toEqual([
{ details: 'GPT 5.4 • 2x' },
{ details: 'Claude Opus 4.7 • 4x' },
]);
});

it('converts file attachments to references on user messages', () => {
const events: any[] = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export function formatModelDetails(model: CopilotCLIModelInfo): string {
return `${model.name}${model.multiplier ? ` • ${model.multiplier}x` : ''}`;
}

export function matchesCopilotCLIModel(model: Pick<CopilotCLIModelInfo, 'id' | 'name'>, modelId: string): boolean {
const normalizedModelId = modelId.trim().toLowerCase();
return model.id.trim().toLowerCase() === normalizedModelId || model.name.trim().toLowerCase() === normalizedModelId;
}

export const ICopilotCLISDK = createServiceIdentifier<ICopilotCLISDK>('ICopilotCLISDK');

export const ICopilotCLIModels = createServiceIdentifier<ICopilotCLIModels>('ICopilotCLIModels');
Expand Down Expand Up @@ -114,7 +119,7 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
}
const models = await this.getModels();
modelId = modelId.trim().toLowerCase();
return models.find(m => m.id.toLowerCase() === modelId || m.name.toLowerCase() === modelId)?.id;
return models.find(m => matchesCopilotCLIModel(m, modelId))?.id;
}
public async getDefaultModel() {
// First item in the list is always the default model (SDK sends the list ordered based on default preference)
Expand All @@ -123,9 +128,9 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
return;
}
const defaultModel = models[0];
const preferredModelId = this.extensionContext.globalState.get<string>(COPILOT_CLI_MODEL_MEMENTO_KEY, defaultModel.id)?.trim()?.toLowerCase();
const preferredModelId = this.extensionContext.globalState.get<string>(COPILOT_CLI_MODEL_MEMENTO_KEY, defaultModel.id)?.trim()?.toLowerCase() ?? defaultModel.id;

return models.find(m => m.id.toLowerCase() === preferredModelId)?.id ?? defaultModel.id;
return models.find(m => matchesCopilotCLIModel(m, preferredModelId))?.id ?? defaultModel.id;
}

public async setDefaultModel(modelId: string | undefined): Promise<void> {
Expand Down
Loading
Loading