diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 503664cead07b..ec0da3bf408d1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -45,9 +45,17 @@ "owner": "typescript", "applyTo": "closedDocuments", "fileLocation": [ - "absolute" + "relative", + "${workspaceFolder}" ], - "pattern": "$tsc", + "source": "ts", + "pattern": { + "regexp": "\\] (.+)\\(([\\d,]+)\\): error TS(\\d+): (.*)$", + "file": 1, + "location": 2, + "code": 3, + "message": 4 + }, "background": { "beginsPattern": "Starting compilation\\.\\.\\.", "endsPattern": "Finished compilation with" @@ -71,7 +79,12 @@ "relative", "${workspaceFolder}" ], - "pattern": "$tsc", + "pattern": { + "regexp": "\\] ([^(]+)\\((\\d+,\\d+)\\): (.*)$", + "file": 1, + "location": 2, + "message": 3 + }, "background": { "beginsPattern": "Starting compilation", "endsPattern": "Finished compilation" diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index 4e09c70f9208f..83f47e797f3cd 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -9,6 +9,7 @@ EventEmitter.defaultMaxListeners = 100; import es from 'event-stream'; import fancyLog from 'fancy-log'; +import * as fs from 'fs'; import glob from 'glob'; import gulp from 'gulp'; import filter from 'gulp-filter'; @@ -172,7 +173,12 @@ const tasks = compilations.map(function (tsconfigFile) { return pipeline; } - const cleanTask = task.define(`clean-extension-${name}`, util.rimraf(out)); + const tsBuildInfoFile = path.join(path.dirname(absolutePath), path.basename(absolutePath, '.json') + '.tsbuildinfo'); + + const cleanTask = task.define(`clean-extension-${name}`, async () => { + await util.rimraf(out)(); + fs.rmSync(tsBuildInfoFile, { force: true }); + }); const transpileTask = task.define(`transpile-extension:${name}`, task.series(cleanTask, () => { const pipeline = createPipeline(false, true, true); diff --git a/eslint.config.js b/eslint.config.js index ca35a088c17ef..24d0abad2184e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -681,6 +681,7 @@ export default tseslint.config( 'src/vs/workbench/contrib/search/browser/replace.ts', 'src/vs/workbench/contrib/search/browser/replaceService.ts', 'src/vs/workbench/contrib/search/browser/searchActionsCopy.ts', + 'src/vs/workbench/contrib/search/browser/searchActionsBase.ts', 'src/vs/workbench/contrib/search/browser/searchActionsFind.ts', 'src/vs/workbench/contrib/search/browser/searchActionsNav.ts', 'src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts', diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 79e48a5d92050..7971447f3142c 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -4579,6 +4579,14 @@ "advanced" ] }, + "github.copilot.chat.cli.autoModel.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "%github.copilot.config.cli.autoModel.enabled%", + "tags": [ + "advanced" + ] + }, "github.copilot.chat.cli.planCommand.enabled": { "type": "boolean", "default": true, diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index cf6d9840f86c2..a31bb0977fdf7 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -408,6 +408,7 @@ "github.copilot.config.cli.forkSessions.enabled": "Enable forking sessions in Copilot CLI.", "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.cli.planCommand.enabled": "Enable the /plan command in Copilot CLI to create implementation plans before coding.", "github.copilot.config.cli.aiGenerateBranchNames.enabled": "Enable AI-generated branch names in Copilot CLI.", "github.copilot.config.cli.isolationOption.enabled": "Enable the isolation mode option for Copilot CLI. When enabled, users can choose between Worktree and Workspace modes.", diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.ts index 8bfcbeaf36769..eb40ee970cdbb 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.ts @@ -524,7 +524,7 @@ export interface RequestIdDetails { * 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): (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, lastResponseDetails?: string): (ChatRequestTurn2 | ChatResponseTurn2)[] { const turns: (ChatRequestTurn2 | ChatResponseTurn2)[] = []; let currentResponseParts: ExtendedChatResponsePart[] = []; const pendingToolInvocations = new Map(); @@ -744,7 +744,7 @@ export function buildChatHistoryFromEvents(sessionId: string, modelId: string | flushPendingAssistantMessage(); if (currentResponseParts.length > 0) { - turns.push(new ChatResponseTurn2(currentResponseParts, {}, '')); + turns.push(new ChatResponseTurn2(currentResponseParts, lastResponseDetails ? { details: lastResponseDetails } : {}, '')); } return turns; diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/common/test/copilotCLITools.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/common/test/copilotCLITools.spec.ts index 0b6a6477d5512..0c39b7e1c3518 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/common/test/copilotCLITools.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/common/test/copilotCLITools.spec.ts @@ -154,6 +154,17 @@ describe('CopilotCLITools', () => { expect((markdownParts[0] as any).value?.value || (markdownParts[0] as any).value).toContain('All tests are passing.'); }); + it('preserves response details on the final rebuilt response turn', () => { + const events: any[] = [ + { 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'); + expect(turns).toHaveLength(2); + const responseTurn = turns[1] as ChatResponseTurn2; + expect(responseTurn.result).toEqual({ details: 'Base • 2x' }); + }); + it('converts file attachments to references on user messages', () => { const events: any[] = [ { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts index 5ba70073994f4..6898c09c7b387 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts @@ -25,6 +25,7 @@ import { IInstantiationService } from '../../../../util/vs/platform/instantiatio import { getCopilotLogger } from './logger'; import { ensureRipgrepShim } from './ripgrepShim'; import { CancellationToken } from '../../../../util/vs/base/common/cancellation'; +import { getModelCapabilitiesDescription } from '../../../conversation/common/languageModelAccess'; export const COPILOT_CLI_REASONING_EFFORT_PROPERTY = 'reasoningEffort'; const COPILOT_CLI_MODEL_MEMENTO_KEY = 'github.copilot.cli.sessionModel'; @@ -59,6 +60,10 @@ export interface ICopilotCLIModels { registerLanguageModelChatProvider(lm: typeof vscode['lm']): void; } +export function formatModelDetails(model: CopilotCLIModelInfo): string { + return `${model.name}${model.multiplier ? ` • ${model.multiplier}x` : ''}`; +} + export const ICopilotCLISDK = createServiceIdentifier('ICopilotCLISDK'); export const ICopilotCLIModels = createServiceIdentifier('ICopilotCLIModels'); @@ -66,7 +71,10 @@ export const ICopilotCLIModels = createServiceIdentifier('ICo export class CopilotCLIModels extends Disposable implements ICopilotCLIModels { declare _serviceBrand: undefined; private _availableModels?: Promise; + /** Synchronously available model infos (includes `auto`). Set once the eager fetch completes. */ + private _resolvedModelInfos?: vscode.LanguageModelChatInformation[]; private readonly _onDidChange = this._register(new Emitter()); + constructor( @ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK, @IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext, @@ -75,20 +83,33 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels { @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); - this._availableModels = this._getAvailableModels(); - // Eagerly fetch available models so that they're ready when needed. - this._availableModels - .then(() => this._onDidChange.fire()) - .catch((error) => { - this.logService.error('[CopilotCLIModels] Failed to fetch available models', error); - }); + this._fetchAndCacheModels(); this._register(this._authenticationService.onDidAuthenticationChange(() => { - // Auth changed which means models could've changed. Fire the event + // Auth changed which means models could've changed. Clear caches and re-fetch. this._availableModels = undefined; + this._resolvedModelInfos = undefined; this._onDidChange.fire(); + this._fetchAndCacheModels(); })); } + + private _fetchAndCacheModels(): void { + const availableModels = this._availableModels = this._getAvailableModels(); + availableModels.then(models => { + // Bail out if a newer fetch has superseded this one (e.g. auth changed mid-flight). + if (this._availableModels !== availableModels) { + return; + } + this._resolvedModelInfos = this._buildModelInfos(models); + this._onDidChange.fire(); + }).catch((error) => { + this.logService.error('[CopilotCLIModels] Failed to fetch available models', error); + }); + } async resolveModel(modelId: string): Promise { + if (modelId.toLowerCase() === 'auto' && this.configurationService.getConfig(ConfigKey.Advanced.CLIAutoModelEnabled)) { + return modelId; + } const models = await this.getModels(); modelId = modelId.trim().toLowerCase(); return models.find(m => m.id.toLowerCase() === modelId || m.name.toLowerCase() === modelId)?.id; @@ -147,7 +168,11 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels { const provider: vscode.LanguageModelChatProvider = { onDidChangeLanguageModelChatInformation: this._onDidChange.event, provideLanguageModelChatInformation: async (_options, _token) => { - return this._provideLanguageModelChatInfo(); + const autoModelEnabled = this.configurationService.getConfig(ConfigKey.Advanced.CLIAutoModelEnabled); + if (!this._authenticationService.anyGitHubSession || !this._resolvedModelInfos) { + return autoModelEnabled ? [buildAutoModel()] : []; + } + return this._resolvedModelInfos; }, provideLanguageModelChatResponse: async (_model, _messages, _options, _progress, _token) => { // Implemented via chat participants. @@ -158,14 +183,15 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels { } }; this._register(lm.registerLanguageModelChatProvider('copilotcli', provider)); + this._onDidChange.fire(); } - private async _provideLanguageModelChatInfo(): Promise { - const models = await this.getModels(); + private _buildModelInfos(models: CopilotCLIModelInfo[]): vscode.LanguageModelChatInformation[] { const isReasoningEffortEnabled = this.configurationService.getConfig(ConfigKey.Advanced.CLIThinkingEffortEnabled); - const modelsInfo = models.map((model, index) => { + const isAutoModelEnabled = this.configurationService.getConfig(ConfigKey.Advanced.CLIAutoModelEnabled); + const modelsInfo: vscode.LanguageModelChatInformation[] = models.map((model, index) => { const multiplier = model.multiplier === undefined ? undefined : `${model.multiplier}x`; - return { + const modelInfo: vscode.LanguageModelChatInformation = { id: model.id, name: model.name, family: model.id, @@ -181,13 +207,40 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels { toolCalling: true }, targetChatSessionType: 'copilotcli', - isDefault: index === 0 // SDK guarantees the first item is the default model + isDefault: !isAutoModelEnabled && index === 0 ? true : undefined, + }; + const tooltip = getModelCapabilitiesDescription(modelInfo) ?? ''; + return { + ...modelInfo, + tooltip }; }); + if (isAutoModelEnabled) { + modelsInfo.unshift(buildAutoModel(models[0])); + } return modelsInfo; } } +function buildAutoModel(defaultModel?: CopilotCLIModelInfo): vscode.LanguageModelChatInformation { + return { + id: 'auto', + name: 'Auto', + tooltip: l10n.t('Auto selects the best model for your request based on capacity and performance.'), + family: defaultModel?.id ?? '', + version: '', + maxInputTokens: defaultModel?.maxInputTokens ?? defaultModel?.maxContextWindowTokens ?? 0, + maxOutputTokens: defaultModel?.maxOutputTokens ?? 0, + isUserSelectable: true, + capabilities: { + imageInput: defaultModel?.supportsVision, + toolCalling: true, + }, + targetChatSessionType: 'copilotcli', + isDefault: true, + }; +} + function buildConfigurationSchema(modelInfo: CopilotCLIModelInfo): vscode.LanguageModelConfigurationSchema | undefined { const effortLevels = modelInfo.supportedReasoningEfforts ?? []; if (effortLevels.length === 0) { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts index 3d670a8797f96..3a47cf7d6ee94 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts @@ -44,7 +44,7 @@ import { ICustomSessionTitleService } from '../common/customSessionTitleService' import { IChatDelegationSummaryService } from '../common/delegationSummaryService'; import { SessionIdForCLI } from '../common/utils'; import { getCopilotCLISessionDir } from './cliHelpers'; -import { getAgentFileNameFromFilePath, ICopilotCLIAgents, ICopilotCLISDK, isEnabledForCopilotCLI } from './copilotCli'; +import { formatModelDetails, getAgentFileNameFromFilePath, ICopilotCLIAgents, ICopilotCLIModels, ICopilotCLISDK, isEnabledForCopilotCLI } from './copilotCli'; import { CopilotCliBridgeSpanProcessor } from './copilotCliBridgeSpanProcessor'; import { CopilotCLISession, ICopilotCLISession } from './copilotcliSession'; import { ICopilotCLISkills } from './copilotCLISkills'; @@ -167,6 +167,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS @IPromptVariablesService private readonly _promptVariablesService: IPromptVariablesService, @IChatDebugFileLoggerService private readonly _debugFileLogger: IChatDebugFileLoggerService, @IPromptsService private readonly _promptsService: IPromptsService, + @ICopilotCLIModels private readonly _copilotCLIModels: ICopilotCLIModels, ) { super(); this.showExternalSessions = this.configurationService.getConfig(ConfigKey.Advanced.CLIShowExternalSessions); @@ -844,7 +845,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS return mapping; }; - const history = buildChatHistoryFromEvents(sessionId, modelId, events, getVSCodeRequestId, this._delegationSummaryService, this.logService, getWorkingDirectory(workspace), defaultModeInstructions); + const lastResponseDetails = await this.getModelDetailsString(modelId); + const history = buildChatHistoryFromEvents(sessionId, modelId, events, getVSCodeRequestId, this._delegationSummaryService, this.logService, getWorkingDirectory(workspace), defaultModeInstructions, lastResponseDetails); if (legacyMappings.length > 0) { void this._chatSessionMetadataStore.updateRequestDetails(sessionId, legacyMappings).catch(error => { @@ -893,6 +895,18 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS }; } + private async getModelDetailsString(modelId: string | undefined): Promise { + if (!modelId) { + return undefined; + } + const models = await this._copilotCLIModels.getModels().catch(ex => { + this.logService.error(ex, 'Failed to get models'); + return []; + }); + const modelInfo = models.find(m => m.id === modelId); + return modelInfo ? formatModelDetails(modelInfo) : undefined; + } + /** * Fork an existing session using the SDK's `forkSession` API. diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts index 31955358d5a93..b8d9ccb56912a 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { AuthenticationSession } from 'vscode'; import { IAuthenticationService } from '../../../../../platform/authentication/common/authentication'; +import { ConfigKey } from '../../../../../platform/configuration/common/configurationService'; import { DefaultsOnlyConfigurationService } from '../../../../../platform/configuration/common/defaultsOnlyConfigurationService'; import { InMemoryConfigurationService } from '../../../../../platform/configuration/test/common/inMemoryConfigurationService'; import { IVSCodeExtensionContext } from '../../../../../platform/extContext/common/extensionContext'; @@ -111,21 +112,22 @@ describe('CopilotCLIModels', () => { disposables.clear(); }); - function createModels(options: { hasSession?: boolean; sdk?: ICopilotCLISDK } = {}): { models: CopilotCLIModels; auth: MockAuthenticationService } { + function createModels(options: { hasSession?: boolean; sdk?: ICopilotCLISDK; configService?: MockConfigurationService } = {}): { models: CopilotCLIModels; auth: MockAuthenticationService; configService: MockConfigurationService } { const auth = new MockAuthenticationService(options.hasSession ?? true); const sdk = options.sdk ?? createMockSDK(); const extensionContext = createMockExtensionContext(); + const configService = options.configService ?? new MockConfigurationService(); const models = new CopilotCLIModels( sdk, extensionContext, logService, auth as unknown as IAuthenticationService, - new MockConfigurationService() + configService ); disposables.add(models); disposables.add({ dispose: () => auth.dispose() }); - return { models, auth }; + return { models, auth, configService }; } describe('getModels', () => { @@ -187,6 +189,15 @@ describe('CopilotCLIModels', () => { expect(await models.resolveModel('nonexistent-model')).toBeUndefined(); }); + + it('resolves "auto" without querying SDK models', async () => { + const { models } = createModels({ hasSession: false }); + + // Even without a session, 'auto' resolves to itself + expect(await models.resolveModel('auto')).toBe('auto'); + expect(await models.resolveModel('Auto')).toBe('Auto'); + expect(await models.resolveModel('AUTO')).toBe('AUTO'); + }); }); describe('getDefaultModel', () => { @@ -340,6 +351,228 @@ describe('CopilotCLIModels', () => { }); }); + describe('provideLanguageModelChatInformation', () => { + function createLmMock() { + let capturedProvider: any; + return { + mock: { + registerLanguageModelChatProvider: (_id: string, provider: any) => { + capturedProvider = provider; + return { dispose: () => { } }; + } + }, + getProvider: () => capturedProvider, + }; + } + + it('always includes auto model in results', async () => { + const { models } = createModels({ hasSession: true }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + // Wait for the eager fetch to complete + await models.getModels(); + // Allow the _fetchAndCacheModels .then() to run + await new Promise(r => setTimeout(r, 0)); + + const result = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(result[0]).toEqual(expect.objectContaining({ id: 'auto', name: 'Auto' })); + }); + + it('returns only auto when not authenticated', async () => { + const { models } = createModels({ hasSession: false }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + // Allow microtasks to settle (the eager fetch will fail/return empty) + await new Promise(r => setTimeout(r, 0)); + + const result = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(result).toEqual([expect.objectContaining({ id: 'auto', name: 'Auto' })]); + }); + + it('returns only auto while models are still being fetched', async () => { + // Create an SDK that never resolves + let resolveModels!: (models: any[]) => void; + const sdk = { + _serviceBrand: undefined, + getPackage: vi.fn(async () => ({ + getAvailableModels: vi.fn(() => new Promise(resolve => { resolveModels = resolve; })), + })), + getAuthInfo: vi.fn(async () => ({ type: 'token' as const, token: 'test-token', host: 'https://github.com' })), + getRequestId: vi.fn(() => undefined), + } as unknown as ICopilotCLISDK; + + const { models } = createModels({ hasSession: true, sdk }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + // Models are still pending — should only get auto + const result = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(result).toEqual([expect.objectContaining({ id: 'auto', name: 'Auto' })]); + + // Flush microtasks so getPackage()/getAuthInfo() resolve and getAvailableModels is called, + // which captures resolveModels. + await new Promise(r => setTimeout(r, 0)); + + // Now resolve the models and let promises settle + resolveModels(FAKE_MODELS.map(m => ({ + id: m.id, name: m.name, + capabilities: { limits: { max_context_window_tokens: m.maxContextWindowTokens, max_prompt_tokens: m.maxInputTokens, max_output_tokens: m.maxOutputTokens }, supports: { vision: m.supportsVision } }, + }))); + await new Promise(r => setTimeout(r, 0)); + + const afterResolve = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(afterResolve.length).toBe(3); // auto + 2 models + expect(afterResolve[0]).toEqual(expect.objectContaining({ id: 'auto' })); + expect(afterResolve[1]).toEqual(expect.objectContaining({ id: 'gpt-4o' })); + expect(afterResolve[2]).toEqual(expect.objectContaining({ id: 'gpt-3.5' })); + }); + + it('returns full model list with auto prepended after fetch completes', async () => { + const { models } = createModels({ hasSession: true }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + // Wait for the eager fetch to complete + await models.getModels(); + await new Promise(r => setTimeout(r, 0)); + + const result = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(result.length).toBe(3); // auto + 2 models + expect(result.map((m: any) => m.id)).toEqual(['auto', 'gpt-4o', 'gpt-3.5']); + }); + + it('resets to auto-only after auth change, then recovers', async () => { + const { models, auth } = createModels({ hasSession: true }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + // Wait for initial fetch + await models.getModels(); + await new Promise(r => setTimeout(r, 0)); + + const beforeAuthChange = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(beforeAuthChange.length).toBe(3); + + // Fire auth change — caches are cleared + auth.fireAuthenticationChange(); + + // Immediately after auth change, _resolvedModelInfos is cleared but re-fetch is in flight. + // Before the re-fetch settles, we should get just auto. + // (The re-fetch is async so hasn't settled yet in the same microtask.) + const duringRefresh = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + // Could be auto-only or already refreshed depending on timing; at minimum auto is present + expect(duringRefresh[0]).toEqual(expect.objectContaining({ id: 'auto' })); + + // Let the re-fetch settle + await models.getModels(); + await new Promise(r => setTimeout(r, 0)); + + const afterRefresh = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(afterRefresh.length).toBe(3); + expect(afterRefresh[0]).toEqual(expect.objectContaining({ id: 'auto' })); + }); + + it('fires onDidChange when models become available', async () => { + let resolveModels!: (models: any[]) => void; + const sdk = { + _serviceBrand: undefined, + getPackage: vi.fn(async () => ({ + getAvailableModels: vi.fn(() => new Promise(resolve => { resolveModels = resolve; })), + })), + getAuthInfo: vi.fn(async () => ({ type: 'token' as const, token: 'test-token', host: 'https://github.com' })), + getRequestId: vi.fn(() => undefined), + } as unknown as ICopilotCLISDK; + + const { models } = createModels({ hasSession: true, sdk }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + let changeCount = 0; + disposables.add(lm.getProvider().onDidChangeLanguageModelChatInformation(() => { changeCount++; })); + + // Flush microtasks so getPackage()/getAuthInfo() resolve and getAvailableModels is called, + // which captures resolveModels. + await new Promise(r => setTimeout(r, 0)); + + // Resolve models + resolveModels(FAKE_MODELS.map(m => ({ + id: m.id, name: m.name, + capabilities: { limits: { max_context_window_tokens: m.maxContextWindowTokens, max_prompt_tokens: m.maxInputTokens, max_output_tokens: m.maxOutputTokens }, supports: { vision: m.supportsVision } }, + }))); + await new Promise(r => setTimeout(r, 0)); + + expect(changeCount).toBeGreaterThan(0); + }); + }); + + describe('CLIAutoModelEnabled setting', () => { + function createLmMock() { + let capturedProvider: any; + return { + mock: { + registerLanguageModelChatProvider: (_id: string, provider: any) => { + capturedProvider = provider; + return { dispose: () => { } }; + } + }, + getProvider: () => capturedProvider, + }; + } + + it('omits auto model from resolved list when disabled', async () => { + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, false); + const { models } = createModels({ hasSession: true, configService }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + await models.getModels(); + await new Promise(r => setTimeout(r, 0)); + + const result = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(result.every((m: any) => m.id !== 'auto')).toBe(true); + expect(result.length).toBe(2); + expect(result[0]).toEqual(expect.objectContaining({ id: 'gpt-4o' })); + }); + + it('returns empty list when not authenticated and auto model disabled', async () => { + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, false); + const { models } = createModels({ hasSession: false, configService }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + await new Promise(r => setTimeout(r, 0)); + + const result = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(result).toEqual([]); + }); + + it('resolveModel does not short-circuit auto when disabled', async () => { + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, false); + const { models } = createModels({ hasSession: true, configService }); + + // With the setting disabled, 'auto' is not a known model so resolveModel returns undefined + expect(await models.resolveModel('auto')).toBeUndefined(); + }); + + it('includes auto model when setting is enabled (default)', async () => { + const { models } = createModels({ hasSession: true }); + const lm = createLmMock(); + models.registerLanguageModelChatProvider(lm.mock as any); + + await models.getModels(); + await new Promise(r => setTimeout(r, 0)); + + const result = await lm.getProvider().provideLanguageModelChatInformation({}, undefined); + expect(result[0]).toEqual(expect.objectContaining({ id: 'auto' })); + expect(result.length).toBe(3); // auto + 2 models + }); + }); + describe('SDK error handling', () => { it('returns empty array when SDK getAvailableModels throws', async () => { const sdk = { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts index f6965ebe5d502..1a3ac8f44aced 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts @@ -44,10 +44,10 @@ import { CopilotCLISession, ICopilotCLISession } from '../copilotcliSession'; import { CopilotCLISessionService, CopilotCLISessionWorkspaceTracker, ICopilotCLISessionItem } from '../copilotcliSessionService'; import { CopilotCLIMCPHandler } from '../mcpHandler'; import { IQuestion, IQuestionAnswer, IUserQuestionHandler } from '../userInputHelpers'; -import { MockCliSdkSession, MockCliSdkSessionManager, MockSkillLocations, NullCopilotCLIAgents, NullICopilotCLIImageSupport } from './testHelpers'; +import { MockCliSdkSession, MockCliSdkSessionManager, MockSkillLocations, NullCopilotCLIAgents, NullCopilotCLIModels, NullICopilotCLIImageSupport } from './testHelpers'; // Re-export for backward compatibility with other spec files -export { MockCliSdkSession, MockCliSdkSessionManager, MockSkillLocations, NullCopilotCLIAgents, NullICopilotCLIImageSupport } from './testHelpers'; +export { MockCliSdkSession, MockCliSdkSessionManager, MockSkillLocations, NullCopilotCLIAgents, NullCopilotCLIModels, NullICopilotCLIImageSupport } from './testHelpers'; class MockLocalSession { static async fromEvents(events: readonly { type: string }[]): Promise<{}> { @@ -156,7 +156,7 @@ describe('CopilotCLISessionService', () => { const configurationService = accessor.get(IConfigurationService); const nullMcpServer = disposables.add(new NullMcpService()); const titleService = new NullCustomSessionTitleService(); - service = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), cliAgents, workspaceService, titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()))); + service = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), cliAgents, workspaceService, titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()), new NullCopilotCLIModels())); manager = await service.getSessionManager() as unknown as MockCliSdkSessionManager; }); @@ -359,7 +359,7 @@ describe('CopilotCLISessionService', () => { return undefined; } }(); - const partialService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()))); + const partialService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()), new NullCopilotCLIModels())); await mkdir(sessionDir.fsPath, { recursive: true }); await writeNodeFile(join(sessionDir.fsPath, 'events.jsonl'), [ @@ -394,7 +394,7 @@ describe('CopilotCLISessionService', () => { const delegationService = new class extends mock() { override extractPrompt(): { prompt: string; reference: never } | undefined { return undefined; } }(); - const partialService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()))); + const partialService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()), new NullCopilotCLIModels())); await mkdir(sessionDir.fsPath, { recursive: true }); const eventsFilePath = join(sessionDir.fsPath, 'events.jsonl'); @@ -463,7 +463,7 @@ describe('CopilotCLISessionService', () => { return undefined; } }(); - const partialService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()))); + const partialService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()), new NullCopilotCLIModels())); const partialManager = await partialService.getSessionManager() as unknown as MockCliSdkSessionManager; const session = new MockCliSdkSession(sessionId, new Date('2024-01-01T00:00:00.000Z')); @@ -505,7 +505,7 @@ describe('CopilotCLISessionService', () => { const delegationService = new class extends mock() { override extractPrompt(): { prompt: string; reference: never } | undefined { return undefined; } }(); - const partialService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()))); + const partialService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), titleService, configurationService, new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()), new NullCopilotCLIModels())); const partialManager = await partialService.getSessionManager() as unknown as MockCliSdkSessionManager; // Session has a summary with '<' (which forces the session-load fallback path) @@ -768,7 +768,7 @@ describe('CopilotCLISessionService', () => { const delegationService = new class extends mock() { override extractPrompt(): { prompt: string; reference: never } | undefined { return undefined; } }(); - const localService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), new NullCustomSessionTitleService(), configurationService, new MockSkillLocations(), delegationService, metadataStore, new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()))); + const localService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), new NullCustomSessionTitleService(), configurationService, new MockSkillLocations(), delegationService, metadataStore, new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()), new NullCopilotCLIModels())); const localManager = await localService.getSessionManager() as unknown as MockCliSdkSessionManager; localManager.sessions.set(sourceId, new MockCliSdkSession(sourceId, new Date())); @@ -812,7 +812,7 @@ describe('CopilotCLISessionService', () => { }(); const metadataStore = new MockChatSessionMetadataStore(); await metadataStore.updateRequestDetails(sourceId, [{ vscodeRequestId: 'vsc-req-1', copilotRequestId: 'sdk-event-1', toolIdEditMap: {} }]); - const localService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), new NullCustomSessionTitleService(), configurationService, new MockSkillLocations(), delegationService, metadataStore, new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()))); + const localService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), new CopilotCLIMCPHandler(logService, authService, configurationService, nullMcpServer), new NullCopilotCLIAgents(), new NullWorkspaceService(), new NullCustomSessionTitleService(), configurationService, new MockSkillLocations(), delegationService, metadataStore, new NullAgentSessionsWorkspace(), new NullChatSessionWorkspaceFolderService(), new NullChatSessionWorktreeService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()), new NullCopilotCLIModels())); const localManager = await localService.getSessionManager() as unknown as MockCliSdkSessionManager; localManager.sessions.set(sourceId, sdkSession); const forkSpy = vi.spyOn(localManager, 'forkSession'); diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/testHelpers.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/testHelpers.ts index 71847f04b6b2f..19ee9639ceb1c 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/testHelpers.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/testHelpers.ts @@ -9,7 +9,7 @@ import { Event } from '../../../../../util/vs/base/common/event'; import { Disposable, IDisposable } from '../../../../../util/vs/base/common/lifecycle'; import { URI } from '../../../../../util/vs/base/common/uri'; import { generateUuid } from '../../../../../util/vs/base/common/uuid'; -import { CLIAgentInfo, ICopilotCLIAgents } from '../copilotCli'; +import { CLIAgentInfo, CopilotCLIModelInfo, ICopilotCLIAgents, ICopilotCLIModels } from '../copilotCli'; import { ICopilotCLIImageSupport } from '../copilotCLIImageSupport'; import { ICopilotCLISkills } from '../copilotCLISkills'; import { ICopilotCLIMCPHandler } from '../mcpHandler'; @@ -107,3 +107,12 @@ export class NullCopilotCLIMCPHandler implements ICopilotCLIMCPHandler { return { mcpConfig: undefined, disposable: Disposable.None }; } } + +export class NullCopilotCLIModels implements ICopilotCLIModels { + _serviceBrand: undefined; + async resolveModel(_modelId: string): Promise { return undefined; } + async getDefaultModel(): Promise { return undefined; } + async setDefaultModel(_modelId: string | undefined): Promise { return; } + async getModels(): Promise { return []; } + registerLanguageModelChatProvider(): void { return; } +} diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index b9f2a2ae63ce6..7f381a538e635 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -45,7 +45,7 @@ import { emptyWorkspaceInfo, getWorkingDirectory, isIsolationEnabled, IWorkspace import { ICustomSessionTitleService } from '../copilotcli/common/customSessionTitleService'; import { IChatDelegationSummaryService } from '../copilotcli/common/delegationSummaryService'; import { getCopilotCLISessionDir } from '../copilotcli/node/cliHelpers'; -import { COPILOT_CLI_REASONING_EFFORT_PROPERTY, ICopilotCLIAgents, ICopilotCLIModels, ICopilotCLISDK, isWelcomeView } from '../copilotcli/node/copilotCli'; +import { COPILOT_CLI_REASONING_EFFORT_PROPERTY, formatModelDetails, ICopilotCLIAgents, ICopilotCLIModels, ICopilotCLISDK, isWelcomeView } from '../copilotcli/node/copilotCli'; import { CopilotCLIPromptResolver } from '../copilotcli/node/copilotcliPromptResolver'; import { builtinSlashSCommands, CopilotCLICommand, copilotCLICommands, ICopilotCLISession } from '../copilotcli/node/copilotcliSession'; import { ICopilotCLISessionItem, ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionService'; @@ -1440,6 +1440,18 @@ export class CopilotCLIChatSessionParticipant extends Disposable { await this.commitWorktreeChangesIfNeeded(request, session.object, token); } + // Build the result before the untitled-session swap below. After the swap, + // the chat UI reloads history from the SDK and discards the in-memory + // result, which would drop our `details` field on the first request. + const models = await this.copilotCLIModels.getModels().catch(ex => { + this.logService.error(ex, 'Failed to get models'); + return []; + }); + const modelInfo = models.find(m => m.id === model?.model); + const result: vscode.ChatResult = modelInfo + ? { details: formatModelDetails(modelInfo) } + : {}; + if (isUntitled && !token.isCancellationRequested) { // Its possible the user tried steering, in that case, we should NOT swap the session item because the session. // Else the messages may get lost (wait CHECK_FOR_STEERING_DELAYms to check if we have pending steering requests) @@ -1450,7 +1462,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { // If we have more requests, that means we had the original request as well as at least one another steering request. // Lets not swap anything here, until all pending requests have been completed. if (pendingRequests.size > 0) { - return; + return result; } } @@ -1462,7 +1474,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable { this.folderRepositoryManager.deleteNewSessionFolder(id); this.sessionItemProvider.swap(chatSessionContext.chatSessionItem, { resource: SessionIdForCLI.getResource(session.object.sessionId), label: request.prompt }); } - return {}; + + return result; } catch (ex) { if (isCancellationError(ex)) { return {}; diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts index 4032a1dd2f8e1..a83848fbae6a5 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts @@ -190,7 +190,7 @@ class FakeChatSessionWorktreeCheckpointService extends mock modelId); getDefaultModel = vi.fn(async () => 'base'); @@ -400,7 +400,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { } } as unknown as IInstantiationService; customSessionTitleService = new CustomSessionTitleService(new MockExtensionContext() as unknown as IVSCodeExtensionContext, accessor.get(IInstantiationService), logService, new MockChatSessionMetadataStore()); - sessionService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, mcpHandler, new NullCopilotCLIAgents(), workspaceService, customSessionTitleService, accessor.get(IConfigurationService), new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), { _serviceBrand: undefined, isAgentSessionsWorkspace: false } as IAgentSessionsWorkspace, workspaceFolderService, worktree, new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()))); + sessionService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), fileSystem, mcpHandler, new NullCopilotCLIAgents(), workspaceService, customSessionTitleService, accessor.get(IConfigurationService), new MockSkillLocations(), delegationService, new MockChatSessionMetadataStore(), { _serviceBrand: undefined, isAgentSessionsWorkspace: false } as IAgentSessionsWorkspace, workspaceFolderService, worktree, new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new NullPromptVariablesService(), new NullChatDebugFileLoggerService(), disposables.add(new MockPromptsService()), models as unknown as ICopilotCLIModels)); manager = await sessionService.getSessionManager() as unknown as MockCliSdkSessionManager; contentProvider = new class extends mock() { @@ -462,11 +462,13 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { const authInfo = await sdk.getAuthInfo(); expect(cliSessions.length).toBe(0); - await participant.createHandler()(request, context, stream, token); + const result = await participant.createHandler()(request, context, stream, token); expect(cliSessions.length).toBe(1); expect(cliSessions[0].requests.length).toBe(1); expect(cliSessions[0].requests[0]).toEqual({ input: { prompt: 'Say hi' }, attachments: [], model: { model: 'base' }, authInfo, token }); + // Result includes the model used so it can be rendered as a footer detail. + expect(result).toEqual({ details: 'Base' }); }); it('uses worktree workingDirectory when isolation is enabled for a new untitled session', async () => { diff --git a/extensions/copilot/src/extension/chronicle/common/standupPrompt.ts b/extensions/copilot/src/extension/chronicle/common/standupPrompt.ts index eaf68d3a42284..3b66f0e8ff2af 100644 --- a/extensions/copilot/src/extension/chronicle/common/standupPrompt.ts +++ b/extensions/copilot/src/extension/chronicle/common/standupPrompt.ts @@ -17,7 +17,7 @@ export interface AnnotatedRef extends RefRow { } /** Sessions query — SQLite dialect, last 24 hours */ -export const SESSIONS_QUERY_SQLITE = `SELECT id, summary, branch, repository, cwd, host_type, created_at, updated_at +export const SESSIONS_QUERY_SQLITE = `SELECT * FROM sessions WHERE updated_at >= datetime('now', '-1 day') ORDER BY updated_at DESC`; @@ -72,10 +72,15 @@ export function buildStandupPrompt( const sessionLines = sessions.map(s => { const branch = s.branch ?? 'unknown'; const repo = s.repository ?? 'unknown'; - const summary = s.summary ?? 'No summary'; + const agent = s.agent_name ?? s.source; // Include turn summaries for this session (first few user messages + assistant responses) const sessionTurns = turns.filter(t => t.session_id === s.id).slice(0, 5); + + // Use first turn's user_message as summary when sessions.summary is empty + const firstTurnMessage = sessionTurns[0]?.user_message; + const summary = s.summary || firstTurnMessage || 'No summary'; + const turnLines = sessionTurns .filter(t => t.user_message || t.assistant_response) .map(t => { @@ -94,7 +99,7 @@ export function buildStandupPrompt( : []; return [ - `- ${s.id} | ${repo} (${branch}) | ${summary} | updated ${s.updated_at}`, + `- ${s.id} | ${repo} (${branch}) | ${agent} | ${summary} | updated ${s.updated_at}`, ...turnLines, ...fileLines, ].join('\n'); @@ -132,7 +137,6 @@ Standup for : - Key files: list 2-3 most important files changed - Tools used: mention key tools if visible (e.g., apply_patch, run_in_terminal, search) - PR: [#123](link) — merged/closed (if applicable) - - Sessions: \`session-id-1\`, \`session-id-2\` **🚧 In Progress** @@ -140,7 +144,6 @@ Standup for : - Summary of current work (1-2 sentences based on turn content) - Key files: list 2-3 most important files being worked on - PR: [#789](link) — draft/open (if applicable) - - Sessions: \`session-id\` Formatting rules: - Use the turn data (user messages AND assistant responses) to understand WHAT was done, not just that something happened diff --git a/extensions/copilot/src/extension/chronicle/vscode-node/sessionStoreTracker.ts b/extensions/copilot/src/extension/chronicle/vscode-node/sessionStoreTracker.ts index bb392321afc63..993f0d134d36c 100644 --- a/extensions/copilot/src/extension/chronicle/vscode-node/sessionStoreTracker.ts +++ b/extensions/copilot/src/extension/chronicle/vscode-node/sessionStoreTracker.ts @@ -185,9 +185,9 @@ export class SessionStoreTracker extends Disposable implements IExtensionContrib private _initSession(sessionId: string, span: ICompletedSpanData): void { this._initializedSessions.add(sessionId); - this._bufferSessionUpsert({ id: sessionId, host_type: 'vscode' }); const sessionSource = (span.attributes[GenAiAttr.AGENT_NAME] as string | undefined) ?? 'unknown'; + this._bufferSessionUpsert({ id: sessionId, host_type: 'vscode', agent_name: sessionSource }); // Track the source of the very first session for firstWrite telemetry if (!this._firstWriteSessionSource) { diff --git a/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts new file mode 100644 index 0000000000000..0f21892246fd4 --- /dev/null +++ b/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { IChatEndpoint } from '../../../platform/networking/common/networking'; +import * as l10n from '@vscode/l10n'; +import type { LanguageModelChatInformation } from 'vscode'; + +/** + * Returns a description of the model's capabilities and intended use cases. + * This is shown in the rich hover when selecting models. + */ +export function getModelCapabilitiesDescription(endpoint: IChatEndpoint | LanguageModelChatInformation): string | undefined { + const name = endpoint.name.toLowerCase(); + const family = endpoint.family.toLowerCase(); + + // Claude models + if (family.includes('claude') || name.includes('claude')) { + if (name.includes('opus')) { + return l10n.t('Most capable Claude model. Excellent for complex analysis, coding tasks, and nuanced creative writing.'); + } + if (name.includes('sonnet')) { + return l10n.t('Balanced Claude model offering strong performance for everyday coding and chat tasks at faster speeds.'); + } + if (name.includes('haiku')) { + return l10n.t('Fastest and most compact Claude model. Ideal for quick responses and simple tasks.'); + } + } + + // GPT models + if (family.includes('gpt') || name.includes('gpt') || family.includes('codex') || name.includes('codex')) { + if (name.includes('codex') || family.includes('codex')) { + if (name.includes('max')) { + return l10n.t('Maximum capability Codex model optimized for complex multi-file refactoring and large codebase understanding.'); + } + if (name.includes('mini')) { + return l10n.t('Lightweight Codex model for quick code completions and simple edits with low latency.'); + } + return l10n.t('OpenAI Codex model specialized for code generation, debugging, and software development tasks.'); + } + if (name.includes('4o')) { + return l10n.t('Optimized GPT-4 model with faster responses and multimodal capabilities.'); + } + if (name.includes('4.1') || name.includes('4-1')) { + return l10n.t('Enhanced GPT-4 model with improved instruction following and coding performance.'); + } + if (name.includes('4')) { + return l10n.t('Reliable GPT-4 model suitable for a wide range of coding and general tasks.'); + } + } + + // Gemini models + if (family.includes('gemini') || name.includes('gemini')) { + if (name.includes('flash')) { + return l10n.t('Fast and efficient Gemini model optimized for quick responses and high throughput.'); + } + if (name.includes('pro')) { + return l10n.t("Google's advanced Gemini Pro model with strong reasoning and coding capabilities."); + } + return l10n.t('Google Gemini model with balanced performance for coding and general assistance.'); + } + + // o1/o3 reasoning models + if (family.includes('o1') || family.includes('o3') || name.includes('o1') || name.includes('o3')) { + if (name.includes('mini')) { + return l10n.t('Compact reasoning model for quick problem-solving with step-by-step thinking.'); + } + return l10n.t('Advanced reasoning model that excels at complex problem-solving, math, and coding challenges.'); + } + + return undefined; +} diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts index addf7d792d568..4dc7cc7b99b3e 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -41,6 +41,7 @@ import { IExtensionContribution } from '../../common/contributions'; import { PromptRenderer } from '../../prompts/node/base/promptRenderer'; import { isImageDataPart } from '../common/languageModelChatMessageHelpers'; import { LanguageModelAccessPrompt } from './languageModelAccessPrompt'; +import { getModelCapabilitiesDescription } from '../common/languageModelAccess'; /** * Markers in the autoModelHint experiment variable that indicate the auto model @@ -101,71 +102,6 @@ function buildConfigurationSchema(endpoint: IChatEndpoint): { configurationSchem }; } -/** - * Returns a description of the model's capabilities and intended use cases. - * This is shown in the rich hover when selecting models. - */ -function getModelCapabilitiesDescription(endpoint: IChatEndpoint): string | undefined { - const name = endpoint.name.toLowerCase(); - const family = endpoint.family.toLowerCase(); - - // Claude models - if (family.includes('claude') || name.includes('claude')) { - if (name.includes('opus')) { - return vscode.l10n.t('Most capable Claude model. Excellent for complex analysis, coding tasks, and nuanced creative writing.'); - } - if (name.includes('sonnet')) { - return vscode.l10n.t('Balanced Claude model offering strong performance for everyday coding and chat tasks at faster speeds.'); - } - if (name.includes('haiku')) { - return vscode.l10n.t('Fastest and most compact Claude model. Ideal for quick responses and simple tasks.'); - } - } - - // GPT models - if (family.includes('gpt') || name.includes('gpt') || family.includes('codex') || name.includes('codex')) { - if (name.includes('codex') || family.includes('codex')) { - if (name.includes('max')) { - return vscode.l10n.t('Maximum capability Codex model optimized for complex multi-file refactoring and large codebase understanding.'); - } - if (name.includes('mini')) { - return vscode.l10n.t('Lightweight Codex model for quick code completions and simple edits with low latency.'); - } - return vscode.l10n.t('OpenAI Codex model specialized for code generation, debugging, and software development tasks.'); - } - if (name.includes('4o')) { - return vscode.l10n.t('Optimized GPT-4 model with faster responses and multimodal capabilities.'); - } - if (name.includes('4.1') || name.includes('4-1')) { - return vscode.l10n.t('Enhanced GPT-4 model with improved instruction following and coding performance.'); - } - if (name.includes('4')) { - return vscode.l10n.t('Reliable GPT-4 model suitable for a wide range of coding and general tasks.'); - } - } - - // Gemini models - if (family.includes('gemini') || name.includes('gemini')) { - if (name.includes('flash')) { - return vscode.l10n.t('Fast and efficient Gemini model optimized for quick responses and high throughput.'); - } - if (name.includes('pro')) { - return vscode.l10n.t("Google's advanced Gemini Pro model with strong reasoning and coding capabilities."); - } - return vscode.l10n.t('Google Gemini model with balanced performance for coding and general assistance.'); - } - - // o1/o3 reasoning models - if (family.includes('o1') || family.includes('o3') || name.includes('o1') || name.includes('o3')) { - if (name.includes('mini')) { - return vscode.l10n.t('Compact reasoning model for quick problem-solving with step-by-step thinking.'); - } - return vscode.l10n.t('Advanced reasoning model that excels at complex problem-solving, math, and coding challenges.'); - } - - return undefined; -} - export class LanguageModelAccess extends Disposable implements IExtensionContribution { readonly id = 'languageModelAccess'; diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 1bad1a2e2ce46..69f4d9951a144 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -693,8 +693,13 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I const strippedMessages = ToolCallingLoop.stripInternalToolCallIds(result.messages); const rawEffort = this.request.modelConfiguration?.reasoningEffort; const isSubagent = !!this.request.subAgentInvocationId; + // Must match the main agent's enableThinking logic in + // toolCallingLoop.ts runOne() — thinking is only disabled + // on continuation turns for Anthropic when no thinking + // blocks exist yet in the messages. + const shouldDisableThinking = !!promptContext.isContinuation && isAnthropicFamily(this.endpoint) && !ToolCallingLoop.messagesContainThinking(strippedMessages); this._lastModelCapabilities = { - enableThinking: !isAnthropicFamily(this.endpoint) || ToolCallingLoop.messagesContainThinking(strippedMessages), + enableThinking: !shouldDisableThinking, reasoningEffort: typeof rawEffort === 'string' ? rawEffort : undefined, enableToolSearch: !isSubagent && !!this.endpoint.supportsToolSearch, enableContextEditing: !isSubagent && isAnthropicContextEditingEnabled(this.endpoint, this.configurationService, this.expService), diff --git a/extensions/copilot/src/extension/intents/node/chronicleIntent.ts b/extensions/copilot/src/extension/intents/node/chronicleIntent.ts index c222b6c5dd81a..05376f288602a 100644 --- a/extensions/copilot/src/extension/intents/node/chronicleIntent.ts +++ b/extensions/copilot/src/extension/intents/node/chronicleIntent.ts @@ -34,11 +34,11 @@ import { PromptRenderer, RendererIntentInvocation } from '../../prompts/node/bas import { ChroniclePrompt } from '../../prompts/node/panel/chroniclePrompt'; /** Cloud SQL dialect sessions query. */ -const SESSIONS_QUERY_CLOUD = `SELECT id, summary, branch, repository, cwd, created_at, updated_at +const SESSIONS_QUERY_CLOUD = `SELECT * FROM sessions WHERE updated_at >= now() - INTERVAL '1 day' ORDER BY updated_at DESC - LIMIT 50`; + LIMIT 100`; const SUBCOMMANDS = ['standup', 'tips', 'improve'] as const; type ChronicleSubcommand = typeof SUBCOMMANDS[number]; @@ -301,7 +301,7 @@ Analysis dimensions to explore: Query guidelines: - Only one query per call — do not combine multiple statements with semicolons. -- Always use LIMIT (max 50) in your queries and prefer aggregations (COUNT, GROUP BY) over raw row dumps. +- Always use LIMIT (max 100) in your queries and prefer aggregations (COUNT, GROUP BY) over raw row dumps. - Use the turns table to understand conversation quality, not just session metadata.`; if (extra) { @@ -335,7 +335,7 @@ User's question: ${userQuery} Use the session_store_sql tool to run queries. Start with a broad query, then drill down as needed. - Only SELECT queries are allowed - Only one query per call — do not combine multiple statements with semicolons -- Always use LIMIT (max 50) and prefer aggregations (COUNT, GROUP BY) over raw row dumps +- Always use LIMIT (max 100) and prefer aggregations (COUNT, GROUP BY) over raw row dumps - Query the **turns** table for conversation content (user_message, assistant_response) — this gives the richest insight into what happened - Query **session_files** for file paths and tool usage patterns - Query **session_refs** for PR/issue/commit links @@ -399,15 +399,15 @@ Use the session_store_sql tool to run queries. Start with a broad query, then dr private _getSchemaDescription(hasCloud: boolean): string { return hasCloud ? `Available tables (cloud SQL syntax): -- **sessions**: id, repository, branch, summary, created_at, updated_at (TIMESTAMP). NOTE: cwd is always NULL in the cloud. -- **turns**: session_id, turn_index, user_message, assistant_response, timestamp (TIMESTAMP). The richest source of what actually happened — contains the user's prompts and the assistant's replies. +- **sessions**: id, repository, branch, summary, agent_name (who created the session, e.g. 'VS Code', 'cli', 'Copilot Coding Agent', 'Copilot Code Review'), agent_description, created_at, updated_at (TIMESTAMP). NOTE: cwd is always NULL in the cloud. IMPORTANT: Always filter on **updated_at** (not created_at) for time ranges — some session types have created_at set to epoch zero. NOTE: summary and repository/branch may be NULL — always JOIN with turns to get actual content. +- **turns**: session_id, turn_index, user_message, assistant_response, timestamp (TIMESTAMP). The richest and most reliable source of what actually happened — the first turn (turn_index=0) user_message is effectively the session summary. Always JOIN sessions with turns for meaningful results. - **session_files**: session_id, file_path, tool_name, turn_index. Tracks which files were read/edited and which tools were used. - **session_refs**: session_id, ref_type (commit/pr/issue), ref_value, turn_index. Tracks PRs created, issues referenced, commits made. Use \`now() - INTERVAL '1 day'\` for date math, \`ILIKE\` for text search. -Join sessions with turns/files/refs using session_id for complete analysis.` +Always JOIN sessions with turns to get session content — do not rely on sessions.summary alone.` : `Available tables (SQLite syntax — local): -- **sessions**: id, cwd, repository, branch, summary, host_type, created_at, updated_at +- **sessions**: id, cwd, repository, branch, summary, host_type, agent_name (who created the session, e.g. 'vscode', 'cli', 'CCA', 'CCR'), agent_description, created_at, updated_at - **turns**: session_id, turn_index, user_message, assistant_response, timestamp. The richest source of what actually happened — contains the user's prompts and the assistant's replies. - **session_files**: session_id, file_path, tool_name, turn_index. Tracks which files were read/edited and which tools were used. - **session_refs**: session_id, ref_type (commit/pr/issue), ref_value, turn_index. Tracks PRs created, issues referenced, commits made. @@ -460,6 +460,8 @@ Join sessions with turns/files/refs using session_id for complete analysis.`; summary: r.summary as string | undefined, branch: r.branch as string | undefined, repository: r.repository as string | undefined, + agent_name: r.agent_name as string | undefined, + agent_description: r.agent_description as string | undefined, created_at: r.created_at as string | undefined, updated_at: r.updated_at as string | undefined, source: 'cloud' as const, diff --git a/extensions/copilot/src/extension/tools/node/sessionStoreSqlTool.ts b/extensions/copilot/src/extension/tools/node/sessionStoreSqlTool.ts index 8727a7d19a7e6..033aa0fdc2a93 100644 --- a/extensions/copilot/src/extension/tools/node/sessionStoreSqlTool.ts +++ b/extensions/copilot/src/extension/tools/node/sessionStoreSqlTool.ts @@ -171,12 +171,20 @@ class SessionStoreSqlTool implements ICopilotTool { } } +/** Max total characters for the formatted result to avoid blowing up the context window. */ +const TOTAL_FORMAT_BUDGET = 30_000; + function formatSqlResult(rows: Record[], truncated: boolean, source: string): string { if (rows.length === 0) { return `No results found (source: ${source}).`; } const columns = Object.keys(rows[0]); + + // Adaptive per-cell limit: distribute budget evenly across all cells + const cellCount = rows.length * columns.length; + const perCellLimit = Math.floor(TOTAL_FORMAT_BUDGET / cellCount); + const lines: string[] = []; lines.push(`Results: ${rows.length} rows (source: ${source})${truncated ? ' [TRUNCATED]' : ''}`); lines.push(''); @@ -189,7 +197,7 @@ function formatSqlResult(rows: Record[], truncated: boolean, so return ''; } const s = String(v); - return s.length > 100 ? s.slice(0, 100) + '...' : s; + return s.length > perCellLimit ? s.slice(0, perCellLimit) + '...' : s; }); lines.push(`| ${values.join(' | ')} |`); } @@ -199,7 +207,14 @@ function formatSqlResult(rows: Record[], truncated: boolean, so lines.push('⚠️ Results were truncated. Add a LIMIT clause or narrow your query.'); } - return lines.join('\n'); + let result = lines.join('\n'); + + // Hard budget enforcement — truncate the entire output if it still exceeds the budget + if (result.length > TOTAL_FORMAT_BUDGET) { + result = result.slice(0, TOTAL_FORMAT_BUDGET) + '\n\n⚠️ Output truncated to stay within context budget.'; + } + + return result; } ToolRegistry.registerTool(SessionStoreSqlTool); diff --git a/extensions/copilot/src/platform/chronicle/common/sessionStore.ts b/extensions/copilot/src/platform/chronicle/common/sessionStore.ts index 7402d1aba2b94..a1881ae7aea77 100644 --- a/extensions/copilot/src/platform/chronicle/common/sessionStore.ts +++ b/extensions/copilot/src/platform/chronicle/common/sessionStore.ts @@ -21,6 +21,8 @@ export interface SessionRow { host_type?: string; branch?: string; summary?: string; + agent_name?: string; + agent_description?: string; created_at?: string; updated_at?: string; } diff --git a/extensions/copilot/src/platform/chronicle/node/sessionStore.ts b/extensions/copilot/src/platform/chronicle/node/sessionStore.ts index a42923a87721e..2348602da2fbc 100644 --- a/extensions/copilot/src/platform/chronicle/node/sessionStore.ts +++ b/extensions/copilot/src/platform/chronicle/node/sessionStore.ts @@ -29,7 +29,7 @@ const READ_ONLY_ACTION_CODES = new Set([ ]); /** Schema version — bump when altering tables so existing DBs get migrated. */ -const SCHEMA_VERSION = 2; +const SCHEMA_VERSION = 3; /** * Session store backed by SQLite + FTS5. @@ -118,6 +118,8 @@ export class SessionStore implements ISessionStore { host_type TEXT, branch TEXT, summary TEXT, + agent_name TEXT, + agent_description TEXT, created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); @@ -193,6 +195,10 @@ export class SessionStore implements ISessionStore { if (currentVersion >= 1 && currentVersion < 2) { db.exec('ALTER TABLE sessions ADD COLUMN host_type TEXT'); } + if (currentVersion >= 1 && currentVersion < 3) { + db.exec('ALTER TABLE sessions ADD COLUMN agent_name TEXT'); + db.exec('ALTER TABLE sessions ADD COLUMN agent_description TEXT'); + } // Update or insert schema version if (currentVersion === 0) { @@ -210,14 +216,16 @@ export class SessionStore implements ISessionStore { upsertSession(session: SessionRow): void { const db = this.ensureDb(); db.prepare( - `INSERT INTO sessions (id, cwd, repository, host_type, branch, summary, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `INSERT INTO sessions (id, cwd, repository, host_type, branch, summary, agent_name, agent_description, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET cwd = COALESCE(excluded.cwd, cwd), repository = COALESCE(excluded.repository, repository), host_type = COALESCE(excluded.host_type, host_type), branch = COALESCE(excluded.branch, branch), summary = COALESCE(excluded.summary, summary), + agent_name = COALESCE(excluded.agent_name, agent_name), + agent_description = COALESCE(excluded.agent_description, agent_description), created_at = MIN(created_at, excluded.created_at), updated_at = MAX(updated_at, excluded.updated_at)`, ).run( @@ -227,6 +235,8 @@ export class SessionStore implements ISessionStore { session.host_type ?? null, session.branch ?? null, session.summary ?? null, + session.agent_name ?? null, + session.agent_description ?? null, session.created_at ?? new Date().toISOString(), session.updated_at ?? new Date().toISOString(), ); diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 506e7cece1358..c8d08d928f70a 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -612,6 +612,7 @@ export namespace ConfigKey { export const OmitBaseAgentInstructions = defineAndMigrateSetting('chat.advanced.omitBaseAgentInstructions', 'chat.omitBaseAgentInstructions', false); export const CLIShowExternalSessions = defineSetting('chat.cli.showExternalSessions', ConfigType.Simple, false); export const CLIPlanExitModeEnabled = defineSetting('chat.cli.planExitMode.enabled', ConfigType.Simple, true); + export const CLIAutoModelEnabled = defineSetting('chat.cli.autoModel.enabled', ConfigType.Simple, true); export const CLIPlanCommandEnabled = defineSetting('chat.cli.planCommand.enabled', ConfigType.Simple, true); export const CLIAIGenerateBranchNames = defineSetting('chat.cli.aiGenerateBranchNames.enabled', ConfigType.Simple, true); export const CLIForkSessionsEnabled = defineSetting('chat.cli.forkSessions.enabled', ConfigType.Simple, true); diff --git a/package.json b/package.json index 76c12a4fafc03..1488774c81373 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.118.0", - "distro": "0cd3df038e4308fc174836ca4eb8faa37e00d5cf", + "distro": "4a9ee0758a3f5eba24d7c00f2fa667d080108b24", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/sessions/browser/parts/media/titlebarpart.css b/src/vs/sessions/browser/parts/media/titlebarpart.css index 1ba528f667341..41edffffa3c87 100644 --- a/src/vs/sessions/browser/parts/media/titlebarpart.css +++ b/src/vs/sessions/browser/parts/media/titlebarpart.css @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container { +.monaco-workbench .part.titlebar > .sessions-titlebar-container { justify-content: initial; } -.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left { +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left { display: flex; height: 100%; align-items: center; @@ -18,7 +18,7 @@ justify-content: flex-start; } -.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container.has-center > .titlebar-center { +.monaco-workbench .part.titlebar > .sessions-titlebar-container.has-center > .titlebar-center { order: 1; width: auto; flex-grow: 0; @@ -28,21 +28,21 @@ justify-content: flex-start; } -.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container.has-center > .titlebar-left { +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left { width: fit-content; flex-grow: 0; } -.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container.has-center > .titlebar-center { +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center { flex: 1; max-width: none; } -.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container.has-center > .titlebar-center .window-title { +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center .window-title { margin: unset; } -.agent-sessions-workbench.monaco-workbench.mac .part.titlebar > .sessions-titlebar-container.has-center > .titlebar-right { +.agent-sessions-workbench.monaco-workbench.mac .part.titlebar > .sessions-titlebar-container > .titlebar-right { order: 2; width: fit-content; flex-grow: 0; @@ -90,7 +90,7 @@ .monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container { display: none; padding-left: 8px; - flex-grow: 1; + flex-grow: 0; flex-shrink: 0; text-align: center; position: relative; @@ -102,7 +102,7 @@ .monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container:not(.has-no-actions) { display: flex; - justify-content: flex-start; + justify-content: center; align-items: center; } @@ -114,14 +114,6 @@ display: flex; } -/* Allow the action bar inside the left toolbar to fill the container so the host filter combo can center itself in the remaining space. */ -.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container, -.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container > .monaco-action-bar, -.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container > .monaco-action-bar > .actions-container { - flex: 1 1 auto; - min-width: 0; -} - /* Remove the titlebar shadow in agent sessions */ .agent-sessions-workbench.monaco-workbench .part.titlebar { box-shadow: none; diff --git a/src/vs/sessions/contrib/changes/browser/changesView.contribution.ts b/src/vs/sessions/contrib/changes/browser/changes.contribution.ts similarity index 72% rename from src/vs/sessions/contrib/changes/browser/changesView.contribution.ts rename to src/vs/sessions/contrib/changes/browser/changes.contribution.ts index 88144bd21e03e..f55765a4c93f8 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.contribution.ts +++ b/src/vs/sessions/contrib/changes/browser/changes.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; -import { localize2 } from '../../../../nls.js'; +import { localize, localize2 } from '../../../../nls.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; @@ -15,6 +15,7 @@ import { ChangesViewPane, ChangesViewPaneContainer } from './changesView.js'; import { ChangesTitleBarContribution } from './changesTitleBarWidget.js'; import './changesViewActions.js'; import './checksActions.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; const changesViewIcon = registerIcon('changes-view-icon', Codicon.gitCompare, localize2('changesViewIcon', 'View icon for the Changes view.').value); @@ -23,12 +24,24 @@ const viewContainersRegistry = Registry.as(ViewContaine const changesViewContainer = viewContainersRegistry.registerViewContainer({ id: CHANGES_VIEW_CONTAINER_ID, title: localize2('changes', 'Changes'), - ctorDescriptor: new SyncDescriptor(ChangesViewPaneContainer), icon: changesViewIcon, order: 10, - hideIfEmpty: true, + ctorDescriptor: new SyncDescriptor(ChangesViewPaneContainer, [CHANGES_VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]), + storageId: CHANGES_VIEW_CONTAINER_ID, + hideIfEmpty: false, + openCommandActionDescriptor: { + id: CHANGES_VIEW_CONTAINER_ID, + mnemonicTitle: localize({ key: 'miChanges', comment: ['&& denotes a mnemonic'] }, "Chan&&ges"), + keybindings: { + primary: 0, + win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG }, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyG }, + }, + order: 1, + }, windowVisibility: WindowVisibility.Sessions -}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); +}, ViewContainerLocation.AuxiliaryBar); const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); @@ -37,8 +50,8 @@ viewsRegistry.registerViews([{ name: localize2('changes', 'Changes'), containerIcon: changesViewIcon, ctorDescriptor: new SyncDescriptor(ChangesViewPane), - canToggleVisibility: true, - canMoveView: true, + canToggleVisibility: false, + canMoveView: false, weight: 100, order: 1, windowVisibility: WindowVisibility.Sessions, diff --git a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts index d802caaabd1c3..e9c4da9760068 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts @@ -5,8 +5,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { alert } from '../../../../base/browser/ui/aria/aria.js'; +import { localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; @@ -14,18 +13,11 @@ import { IViewsService } from '../../../../workbench/services/views/common/views import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { ActiveSessionContextKeys, CHANGES_VIEW_CONTAINER_ID, CHANGES_VIEW_ID } from '../common/changes.js'; +import { ActiveSessionContextKeys, CHANGES_VIEW_ID } from '../common/changes.js'; import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IPaneCompositePartService } from '../../../../workbench/services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../../workbench/common/views.js'; import { ChangesViewPane } from './changesView.js'; -import { SESSIONS_FILES_CONTAINER_ID } from '../../files/browser/files.contribution.js'; -import { SESSIONS_FILES_VIEW_ID } from '../../files/browser/filesView.js'; import { URI } from '../../../../base/common/uri.js'; import { isEqual } from '../../../../base/common/resources.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; @@ -53,63 +45,6 @@ class OpenChangesViewAction extends Action2 { registerAction2(OpenChangesViewAction); -registerAction2(class FocusChangesViewAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.agentSessions.focusChangesView', - title: localize2('focusChangesView', "Focus Changes View"), - category: Categories.View, - precondition: IsSessionsWindowContext, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG, - when: IsSessionsWindowContext, - }, - }); - } - async run(accessor: ServicesAccessor): Promise { - const sessionManagementService = accessor.get(ISessionsManagementService); - const activeSession = sessionManagementService.activeSession.get(); - const changes = activeSession?.changes.get(); - if (!changes || changes.length === 0) { - alert(localize('focusChangesView.noChanges', "There are no changes.")); - return; - } - const paneCompositeService = accessor.get(IPaneCompositePartService); - const viewsService = accessor.get(IViewsService); - await paneCompositeService.openPaneComposite(CHANGES_VIEW_CONTAINER_ID, ViewContainerLocation.AuxiliaryBar, true); - const view = await viewsService.openView(CHANGES_VIEW_ID, true); - view?.focus(); - } -}); - -registerAction2(class FocusChangesFileViewAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.agentSessions.focusChangesFileView', - title: localize2('focusChangesFileView', "Focus Files Explorer View"), - category: Categories.View, - precondition: IsSessionsWindowContext, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib + 1, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyE, - when: IsSessionsWindowContext, - }, - }); - } - async run(accessor: ServicesAccessor): Promise { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const viewsService = accessor.get(IViewsService); - await paneCompositeService.openPaneComposite(SESSIONS_FILES_CONTAINER_ID, ViewContainerLocation.AuxiliaryBar, true); - const view = await viewsService.openView(SESSIONS_FILES_VIEW_ID, true); - if (view) { - view.focus(); - } - } -}); - class ChangesViewActionsContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.changesViewActions'; diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css index fd0ee642e7d89..26fc0d59d66fa 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css @@ -174,10 +174,6 @@ margin-left: 4px; } -.chat-controls-container .monaco-editor-background { - background-color: var(--vscode-input-background) !important; -} - /* Pickers row - two equal halves */ .session-workspace-picker { display: flex; diff --git a/src/vs/sessions/contrib/files/browser/files.contribution.ts b/src/vs/sessions/contrib/files/browser/files.contribution.ts index 6ca93d7c8494b..f472c507c069f 100644 --- a/src/vs/sessions/contrib/files/browser/files.contribution.ts +++ b/src/vs/sessions/contrib/files/browser/files.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; -import { localize2 } from '../../../../nls.js'; +import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; @@ -18,6 +18,7 @@ import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/vie import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; import { WorkspaceFolderCountContext } from '../../../../workbench/common/contextkeys.js'; import { SESSIONS_FILES_EMPTY_VIEW_ID, SESSIONS_FILES_VIEW_ID, SessionsExplorerEmptyView, SessionsExplorerView } from './filesView.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; export const SESSIONS_FILES_CONTAINER_ID = 'workbench.sessions.auxiliaryBar.filesContainer'; @@ -33,9 +34,16 @@ const filesViewContainer = viewContainerRegistry.registerViewContainer({ order: 11, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [SESSIONS_FILES_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]), storageId: SESSIONS_FILES_CONTAINER_ID, - hideIfEmpty: true, + hideIfEmpty: false, + openCommandActionDescriptor: { + id: SESSIONS_FILES_CONTAINER_ID, + title: localize2('explore', "Explorer"), + mnemonicTitle: localize({ key: 'miFiles', comment: ['&& denotes a mnemonic'] }, "Fil&&es"), + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyE }, + order: 0 + }, windowVisibility: WindowVisibility.Sessions, -}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true, isDefault: true }); +}, ViewContainerLocation.AuxiliaryBar, { isDefault: true }); class RegisterFilesViewContribution implements IWorkbenchContribution { @@ -50,7 +58,7 @@ class RegisterFilesViewContribution implements IWorkbenchContribution { name: localize2('files', "Files"), containerIcon: filesViewIcon, ctorDescriptor: new SyncDescriptor(SessionsExplorerView), - canToggleVisibility: true, + canToggleVisibility: false, canMoveView: false, when: WorkspaceFolderCountContext.notEqualsTo('0'), windowVisibility: WindowVisibility.Sessions, @@ -62,7 +70,7 @@ class RegisterFilesViewContribution implements IWorkbenchContribution { name: localize2('files', "Files"), containerIcon: filesViewIcon, ctorDescriptor: new SyncDescriptor(SessionsExplorerEmptyView), - canToggleVisibility: true, + canToggleVisibility: false, canMoveView: false, when: WorkspaceFolderCountContext.isEqualTo('0'), windowVisibility: WindowVisibility.Sessions, diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index 935209325e3f4..76bb00bc718ee 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -125,7 +125,6 @@ import '../workbench/services/authentication/browser/authenticationQueryService. import '../platform/hover/browser/hoverService.js'; import '../platform/userInteraction/browser/userInteractionServiceImpl.js'; import '../workbench/services/assignment/common/assignmentService.js'; -import '../workbench/services/outline/browser/outlineService.js'; import '../workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.js'; import '../editor/common/services/languageFeaturesService.js'; import '../editor/common/services/semanticTokensStylingService.js'; @@ -248,12 +247,7 @@ import '../workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.js'; // Rename Symbol Tracker for Inline completions. import '../workbench/contrib/inlineCompletions/browser/renameSymbolTrackerService.js'; -// Search -import '../workbench/contrib/search/browser/search.contribution.js'; -import '../workbench/contrib/search/browser/searchView.js'; -// Search Editor -import '../workbench/contrib/searchEditor/browser/searchEditor.contribution.js'; // Sash import '../workbench/contrib/sash/browser/sash.contribution.js'; @@ -266,10 +260,11 @@ import '../workbench/contrib/scm/browser/quickDiff.contribution.js'; import '../workbench/contrib/scm/browser/scm.service.contribution.js'; // Debug (service) -import '../workbench/contrib/debug/browser/debug.service.contribution.js'; - -// Markers -import '../workbench/contrib/markers/browser/markers.contribution.js'; +import { NullDebugService, NullDebugVisualizerService } from '../workbench/contrib/debug/common/nullDebugService.js'; +import { IDebugService } from '../workbench/contrib/debug/common/debug.js'; +import { IDebugVisualizerService } from '../workbench/contrib/debug/common/debugVisualizers.js'; +registerSingleton(IDebugService, NullDebugService, InstantiationType.Delayed); +registerSingleton(IDebugVisualizerService, NullDebugVisualizerService, InstantiationType.Delayed); // Process Explorer import '../workbench/contrib/processExplorer/browser/processExplorer.contribution.js'; @@ -299,8 +294,9 @@ import '../workbench/contrib/customEditor/browser/customEditor.contribution.js'; import '../workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.js'; // Extensions Management -import '../workbench/contrib/extensions/browser/extensions.contribution.js'; -import '../workbench/contrib/extensions/browser/extensionsViewlet.js'; +import { IExtensionsWorkbenchService } from '../workbench/contrib/extensions/common/extensions.js'; +import { ExtensionsWorkbenchService } from '../workbench/contrib/extensions/browser/extensionsWorkbenchService.js'; +registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); // Output View import '../workbench/contrib/output/browser/output.contribution.js'; @@ -335,7 +331,7 @@ import '../workbench/contrib/markdown/browser/markdown.contribution.js'; import '../workbench/contrib/keybindings/browser/keybindings.contribution.js'; // Snippets -import '../workbench/contrib/snippets/browser/snippets.contribution.js'; +import '../workbench/contrib/snippets/browser/snippets.service.contribution.js'; // Formatter Help import '../workbench/contrib/format/browser/format.contribution.js'; @@ -355,14 +351,7 @@ import '../workbench/contrib/themes/browser/themes.contribution.js'; // Update import '../workbench/contrib/update/browser/update.contribution.js'; -// Surveys -import '../workbench/contrib/surveys/browser/nps.contribution.js'; -import '../workbench/contrib/surveys/browser/languageSurveys.contribution.js'; - // Welcome -// import '../workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js'; -// import '../workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.contribution.js'; -// import '../workbench/contrib/welcomeWalkthrough/browser/walkThrough.contribution.js'; import '../workbench/contrib/welcomeViews/common/viewsWelcome.contribution.js'; import '../workbench/contrib/welcomeViews/common/newFile.contribution.js'; @@ -373,15 +362,11 @@ import '../workbench/contrib/callHierarchy/browser/callHierarchy.contribution.js import '../workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.js'; // Outline +import '../workbench/services/outline/browser/outlineService.js'; import '../workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.js'; -import '../workbench/contrib/outline/browser/outline.contribution.js'; - // Language Detection import '../workbench/contrib/languageDetection/browser/languageDetection.contribution.js'; -// Language Status -import '../workbench/contrib/languageStatus/browser/languageStatus.contribution.js'; - // Authentication import '../workbench/contrib/authentication/browser/authentication.contribution.js'; @@ -415,15 +400,9 @@ import '../workbench/contrib/list/browser/list.contribution.js'; // Accessibility Signals import '../workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.js'; -// Bracket Pair Colorizer 2 Telemetry -import '../workbench/contrib/bracketPairColorizer2Telemetry/browser/bracketPairColorizer2Telemetry.contribution.js'; - // Accessibility import '../workbench/contrib/accessibility/browser/accessibility.contribution.js'; -// Metered Connection -import '../workbench/contrib/meteredConnection/browser/meteredConnection.contribution.js'; - // Share import '../workbench/contrib/share/browser/share.contribution.js'; @@ -460,7 +439,7 @@ import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/views/sessionsListModelService.js'; import './contrib/remoteAgentHost/browser/agentHostFilterService.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; -import './contrib/changes/browser/changesView.contribution.js'; +import './contrib/changes/browser/changes.contribution.js'; import './contrib/layout/browser/layout.contribution.js'; import './contrib/codeReview/browser/codeReview.contributions.js'; import './contrib/files/browser/files.contribution.js'; diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index f2764417d6ead..43e6e573df850 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -121,8 +121,6 @@ import '../workbench/contrib/codeEditor/electron-browser/codeEditor.contribution // Debug import '../workbench/contrib/debug/electron-browser/extensionHostDebugService.js'; -// Extensions Management -import '../workbench/contrib/extensions/electron-browser/extensions.contribution.js'; // Issues import '../workbench/contrib/issue/electron-browser/issue.contribution.js'; @@ -173,7 +171,6 @@ import '../workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contributio import '../workbench/contrib/remoteTunnel/electron-browser/remoteTunnel.contribution.js'; // Chat -import '../workbench/contrib/chat/electron-browser/chat.contribution.js'; import './contrib/agentFeedback/browser/agentFeedback.contribution.js'; import './contrib/chat/electron-browser/openInVSCode.contribution.js'; diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 8ec211b8a66d5..2a6a562efad8f 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -160,7 +160,8 @@ export class ObservableChatSession extends Disposable implements IChatSession { return { type: 'response' as const, parts: turn.parts.map((part: IChatProgressDto) => revive(part) as IChatProgress), - participant: turn.participant + participant: turn.participant, + details: turn.details, }; })); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4634caf814add..922e84784d0d6 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3659,6 +3659,7 @@ export type IChatSessionHistoryItemDto = { type: 'response'; parts: IChatProgressDto[]; participant: string; + details?: string; }; export type IChatSessionRequestHistoryItemDto = Extract; diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index ac51bc11a21a7..7653e08d8a461 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -1050,7 +1050,8 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio return { type: 'response' as const, parts, - participant: turn.participant + participant: turn.participant, + details: turn.result?.details, }; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 78e9a277ec66e..46107d6555bed 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -450,7 +450,7 @@ export class OpenPermissionPickerAction extends Action2 { precondition: ChatContextKeys.enabled, menu: { id: MenuId.ChatInputSecondary, - order: 10, + order: 1, group: 'navigation', when: ContextKeyExpr.and( @@ -618,23 +618,12 @@ export class OpenWorkspacePickerAction extends Action2 { f1: false, precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.inAgentSessionsWelcome), menu: [ - { - id: MenuId.ChatInput, - order: 0.6, - when: ContextKeyExpr.and( - ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), - IsSessionsWindowContext - ), - group: 'navigation', - }, { id: MenuId.ChatInputSecondary, order: 0.6, when: ContextKeyExpr.and( ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), - IsSessionsWindowContext.negate() + ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType) ), group: 'navigation', }, @@ -656,22 +645,51 @@ export class ChatSessionPrimaryPickerAction extends Action2 { category: CHAT_CATEGORY, f1: false, precondition: ChatContextKeys.enabled, - menu: { - id: MenuId.ChatInput, - order: 4, - group: 'navigation', - when: - ContextKeyExpr.and( - ChatContextKeys.chatSessionHasModels, - ContextKeyExpr.or( - ChatContextKeys.lockedToCodingAgent, - ContextKeyExpr.and( - ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.notEqualsTo('local') + menu: [ + { + // Cloud sessions: keep on the primary chat input toolbar + id: MenuId.ChatInput, + order: 4, + group: 'navigation', + when: + ContextKeyExpr.and( + ChatContextKeys.chatSessionHasModels, + ChatContextKeys.chatSessionType.isEqualTo(AgentSessionProviders.Cloud), + ContextKeyExpr.or( + ChatContextKeys.lockedToCodingAgent, + ContextKeyExpr.and( + ChatContextKeys.inAgentSessionsWelcome, + ChatContextKeys.chatSessionType.notEqualsTo('local') + ) ) ) - ) - } + }, + { + // All other coding agents (Claude, etc.): show in the secondary toolbar. + // In the Agents window only, hide the worktree/branch pickers for Copilot + // CLI sessions because their option groups are surfaced through the CLI + // session UI there. They remain visible in the regular VS Code workbench. + id: MenuId.ChatInputSecondary, + order: 4, + group: 'navigation', + when: + ContextKeyExpr.and( + ChatContextKeys.chatSessionHasModels, + ChatContextKeys.chatSessionType.notEqualsTo(AgentSessionProviders.Cloud), + ContextKeyExpr.or( + IsSessionsWindowContext.negate(), + ChatContextKeys.chatSessionType.notEqualsTo(AgentSessionProviders.Background) + ), + ContextKeyExpr.or( + ChatContextKeys.lockedToCodingAgent, + ContextKeyExpr.and( + ChatContextKeys.inAgentSessionsWelcome, + ChatContextKeys.chatSessionType.notEqualsTo('local') + ) + ) + ) + }, + ] }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts index 2e43c35e95454..19aff2cff013b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts @@ -19,7 +19,7 @@ import { ChatConfiguration } from '../../common/constants.js'; import { IAgentPluginRepositoryService } from '../../common/plugins/agentPluginRepositoryService.js'; import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js'; import { type IMarketplaceReference, MarketplaceReferenceKind, parseMarketplaceReference, parseMarketplaceReferences } from '../../common/plugins/pluginMarketplaceService.js'; -import { InstalledAgentPluginsViewId } from '../agentPluginsView.js'; +import { InstalledAgentPluginsViewId } from '../chat.js'; import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js'; export class ManagePluginsAction extends Action2 { diff --git a/src/vs/workbench/contrib/chat/browser/actions/createPluginAction.ts b/src/vs/workbench/contrib/chat/browser/actions/createPluginAction.ts index 9cda7dadaa20a..03e96021ee1a4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/createPluginAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/createPluginAction.ts @@ -20,7 +20,7 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; import { IQuickInputButton, IQuickInputService, IQuickTreeItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { InstalledAgentPluginsViewId } from '../agentPluginsView.js'; +import { InstalledAgentPluginsViewId } from '../chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { IPromptPath, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts index d16d3df28efaf..329a7c6ed9870 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts @@ -21,7 +21,7 @@ import { dirname } from '../../../../base/common/resources.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; @@ -49,9 +49,7 @@ import { hasSourceChanged, IMarketplacePlugin, IPluginMarketplaceService } from import { AgentPluginEditorInput } from './agentPluginEditor/agentPluginEditorInput.js'; import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem, IMarketplacePluginItem } from './agentPluginEditor/agentPluginItems.js'; import { getInstalledPluginContextMenuActions, InstallPluginAction, OpenPluginReadmeAction } from './agentPluginActions.js'; - -export const HasInstalledAgentPluginsContext = new RawContextKey('hasInstalledAgentPlugins', false); -export const InstalledAgentPluginsViewId = 'workbench.views.agentPlugins.installed'; +import { InstalledAgentPluginsViewId, HasInstalledAgentPluginsContext } from './chat.js'; //#region Item model diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 422268fb8f4b9..9fe5a1b13091c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -158,7 +158,6 @@ import { IAgentPluginRepositoryService } from '../common/plugins/agentPluginRepo import { IPluginInstallService } from '../common/plugins/pluginInstallService.js'; import { IPluginMarketplaceService, PluginMarketplaceService } from '../common/plugins/pluginMarketplaceService.js'; import { WorkspacePluginSettingsService, IWorkspacePluginSettingsService } from '../common/plugins/workspacePluginSettingsService.js'; -import { AgentPluginsViewsContribution } from './agentPluginsView.js'; import { AgentPluginRecommendations } from './claudePluginRecommendations.js'; import { AgentPluginEditor } from './agentPluginEditor/agentPluginEditor.js'; import { AgentPluginEditorInput } from './agentPluginEditor/agentPluginEditorInput.js'; @@ -2142,7 +2141,6 @@ registerWorkbenchContribution2(UserToolSetsContributions.ID, UserToolSetsContrib registerWorkbenchContribution2(PromptLanguageFeaturesProvider.ID, PromptLanguageFeaturesProvider, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatWindowNotifier.ID, ChatWindowNotifier, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatRepoInfoContribution.ID, ChatRepoInfoContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(AgentPluginsViewsContribution.ID, AgentPluginsViewsContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(AgentPluginRecommendations.ID, AgentPluginRecommendations, WorkbenchPhase.Eventually); registerChatActions(); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 71b8aa8ac0110..f5b3e5a934eae 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -11,7 +11,7 @@ import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Selection } from '../../../../editor/common/core/selection.js'; import { EditDeltaInfo } from '../../../../editor/common/textModelEditSource.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { PreferredGroup } from '../../../services/editor/common/editorService.js'; import { IChatAgentAttachmentCapabilities, IChatAgentCommand, IChatAgentData } from '../common/participants/chatAgents.js'; @@ -459,3 +459,6 @@ export interface IChatCodeBlockContextProviderService { export const ChatViewId = `workbench.panel.chat.view.${CHAT_PROVIDER_ID}`; export const ChatViewContainerId = 'workbench.panel.chat'; + +export const HasInstalledAgentPluginsContext = new RawContextKey('hasInstalledAgentPlugins', false); +export const InstalledAgentPluginsViewId = 'workbench.views.agentPlugins.installed'; diff --git a/src/vs/workbench/contrib/chat/browser/chat.view.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.view.contribution.ts new file mode 100644 index 0000000000000..37bf962d03a55 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chat.view.contribution.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { AgentPluginsViewsContribution } from './agentPluginsView.js'; + +registerWorkbenchContribution2(AgentPluginsViewsContribution.ID, AgentPluginsViewsContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 95b4dfe2d850c..61885a725c0b4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -139,7 +139,7 @@ const INPUT_EDITOR_MAX_HEIGHT = 250; const INPUT_EDITOR_LINE_HEIGHT = 20; const INPUT_EDITOR_PADDING = { compact: { top: 2, bottom: 2 }, default: { top: 12, bottom: 12 } }; const CachedLanguageModelsKey = 'chat.cachedLanguageModels.v2'; -const CHAT_INPUT_PICKER_COLLAPSE_WIDTH = 320; +const CHAT_INPUT_PICKER_COLLAPSE_WIDTH = 480; export interface IChatInputStyles { overlayBackground: string; @@ -2290,6 +2290,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge actionContext: { widget }, hideChevrons: derived(reader => this._stableInputPartWidth.read(reader) < CHAT_INPUT_PICKER_COLLAPSE_WIDTH), }; + const secondaryPickerOptions: IChatInputPickerOptions = { + ...pickerOptions, + getOverflowAnchor: () => this.secondaryToolbar.getElement(), + }; this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); @@ -2377,19 +2381,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider; const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions); - } else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) { - if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) { - return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions); - } else { - return new HiddenActionViewItem(action); - } } else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) { - // Create all pickers and return a container action view item + // Cloud sessions render their option-group pickers (e.g. branch) on the primary toolbar const widgets = this.createChatSessionPickerWidgets(action, pickerOptions); if (widgets.length === 0) { return new HiddenActionViewItem(action); } - // Create a container to hold all picker widgets return this.instantiationService.createInstance(ChatSessionPickersContainerActionItem, action, widgets); } return undefined; @@ -2398,11 +2395,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputActionsToolbar.getElement().classList.add('chat-input-toolbar'); this.inputActionsToolbar.context = { widget } satisfies IChatExecuteActionContext; this._register(this.inputActionsToolbar.onDidChangeMenuItems(() => { - // Update container reference for the pickers + // Update container reference for the pickers (cloud sessions host them in the primary toolbar) const toolbarElement = this.inputActionsToolbar.getElement(); // eslint-disable-next-line no-restricted-syntax - const container = toolbarElement.querySelector('.chat-sessionPicker-container'); - this.chatSessionPickerContainer = container as HTMLElement | undefined; + const primaryPickerContainer = toolbarElement.querySelector('.chat-sessionPicker-container'); + if (primaryPickerContainer) { + this.chatSessionPickerContainer = primaryPickerContainer as HTMLElement; + } if (this.cachedWidth && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { this._toolbarRelayoutScheduler.schedule(); } @@ -2451,6 +2450,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge menuOptions: { shouldForwardArgs: true }, hiddenItemStrategy: HiddenItemStrategy.NoHide, hoverDelegate, + responsiveBehavior: { + enabled: true, + kind: 'last', + minItems: 1, + actionMinWidth: 48, + }, actionViewItemProvider: (action, options) => { if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) { const getActiveSessionType = () => { @@ -2476,16 +2481,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }; const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider; const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; - return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions); + return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, secondaryPickerOptions); } else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) { if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) { - return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions); + return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, secondaryPickerOptions); } else { - const empty = new BaseActionViewItem(undefined, action); - if (empty.element) { - empty.element.style.display = 'none'; - } - return empty; + return new HiddenActionViewItem(action); } } else if (action.id === OpenPermissionPickerAction.ID && action instanceof MenuItemAction) { const delegate: IPermissionPickerDelegate = { @@ -2522,7 +2523,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.permissionWidget?.refresh(); }, }; - const widget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, pickerOptions); + const widget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, secondaryPickerOptions); this.permissionWidget = widget; widget.onDidDispose(() => { if (this.permissionWidget === widget) { @@ -2530,12 +2531,31 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } }); return widget; + } else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) { + // Create all pickers and return a container action view item + const widgets = this.createChatSessionPickerWidgets(action, secondaryPickerOptions); + if (widgets.length === 0) { + return new HiddenActionViewItem(action); + } + // Create a container to hold all picker widgets + return this.instantiationService.createInstance(ChatSessionPickersContainerActionItem, action, widgets); } return undefined; } })); this.secondaryToolbar.getElement().classList.add('chat-secondary-input-toolbar'); this.secondaryToolbar.context = { widget } satisfies IChatExecuteActionContext; + this._register(this.secondaryToolbar.onDidChangeMenuItems(() => { + // Update container reference for the pickers when the secondary toolbar hosts one. + // Only assign when found so we don't overwrite a valid primary container reference + // for session types whose pickers live in the primary toolbar (e.g. cloud). + const toolbarElement = this.secondaryToolbar.getElement(); + // eslint-disable-next-line no-restricted-syntax + const container = toolbarElement.querySelector('.chat-sessionPicker-container'); + if (dom.isHTMLElement(container)) { + this.chatSessionPickerContainer = container; + } + })); let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index 282a93c0d58c9..ee9c7f4a7eb15 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -262,7 +262,7 @@ export function buildModelPickerItems( }; // --- 1. Auto --- - const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); + const autoModel = models.find(m => isAutoModel(m)); if (autoModel) { markPlaced(autoModel.identifier, autoModel.metadata.id); items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect, languageModelsService!), autoModel)); @@ -426,7 +426,7 @@ export function buildModelPickerItems( } } else { // Flat list: auto first, then all models sorted alphabetically - const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); + const autoModel = models.find(m => isAutoModel(m)); if (autoModel) { items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect, languageModelsService!), autoModel)); } @@ -794,7 +794,7 @@ export class ModelPickerWidget extends Disposable { function getModelHoverContent(model: ILanguageModelChatMetadataAndIdentifier): MarkdownString { - const isAuto = model.metadata.id === 'auto' && model.metadata.vendor === 'copilot'; + const isAuto = isAutoModel(model); const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); markdown.appendMarkdown(`**${model.metadata.name}**`); @@ -825,3 +825,7 @@ function formatTokenCount(count: number): string { } return count.toString(); } + +function isAutoModel(model: ILanguageModelChatMetadataAndIdentifier): boolean { + return model.metadata.id === 'auto' && (model.metadata.vendor === 'copilot' || model.metadata.vendor === 'copilotcli'); +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 50e0163d2e50e..0fc4f63429ec5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1517,6 +1517,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-secondary-toolbar > .chat-secondary-input-toolbar { overflow: hidden; min-width: 0px; + flex: 1 1 auto; color: var(--vscode-icon-foreground); .monaco-action-bar .action-item .codicon { @@ -1673,6 +1674,14 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-icon-foreground); } +.interactive-session .chat-secondary-input-toolbar .chat-sessionPicker-item .action-label { + height: 16px; + padding: 3px 0px 3px 6px; + display: flex; + align-items: center; + color: var(--vscode-icon-foreground); +} + /* Keep hover background while picker dropdown is open */ .interactive-session .chat-input-toolbar .action-label[aria-expanded="true"], .interactive-session .chat-secondary-toolbar .action-label[aria-expanded="true"] { @@ -1682,7 +1691,8 @@ have to be updated for changes to the rules above, or to support more deeply nes /* When chevrons are hidden and only showing an icon (no label), size to 22x22 with centered icon */ .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.hide-chevrons:not(:has(.chat-input-picker-label)), .interactive-session .chat-input-toolbar .chat-input-picker-item.hide-chevrons .action-label:not(:has(.chat-input-picker-label)), -.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.hide-chevrons:not(:has(.chat-input-picker-label)) { +.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.hide-chevrons:not(:has(.chat-input-picker-label)), +.interactive-session .chat-secondary-input-toolbar .chat-sessionPicker-item .action-label.hide-chevrons:not(:has(.chat-input-picker-label)) { width: 22px; min-width: 22px; height: 22px; @@ -1710,7 +1720,8 @@ have to be updated for changes to the rules above, or to support more deeply nes } .monaco-workbench .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label .codicon-chevron-down, -.monaco-workbench .interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label .codicon-chevron-down { +.monaco-workbench .interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label .codicon-chevron-down, +.monaco-workbench .interactive-session .chat-secondary-input-toolbar .chat-sessionPicker-item .action-label .codicon-chevron-down { font-size: 10px; margin-left: 4px; opacity: 0.75; @@ -1722,8 +1733,10 @@ have to be updated for changes to the rules above, or to support more deeply nes gap: 4px; } -.interactive-session .chat-input-toolbars .chat-sessionPicker-container { +.interactive-session .chat-input-toolbars .chat-sessionPicker-container, +.interactive-session .chat-secondary-toolbar .chat-sessionPicker-container { display: flex; + gap: 4px; max-width: 100%; } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 2f93372484952..0b84592f6e78e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -704,6 +704,9 @@ export class ChatService extends Disposable implements IChatService { for (const part of message.parts) { model.acceptResponseProgress(lastRequest, part); } + if (message.details && lastRequest.response) { + lastRequest.response.setResult({ details: message.details }); + } } } } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 66957b66c5d3e..1701c7433f9e8 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -163,6 +163,7 @@ export type IChatSessionHistoryItem = { type: 'response'; parts: IChatProgress[]; participant: string; + details?: string; }; export type IChatSessionRequestHistoryItem = Extract; diff --git a/src/vs/workbench/contrib/debug/common/nullDebugService.ts b/src/vs/workbench/contrib/debug/common/nullDebugService.ts new file mode 100644 index 0000000000000..02a0b741492b9 --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/nullDebugService.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { Disposable, IDisposable, IReference } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IAdapterManager, IBreakpoint, IConfig, IConfigurationManager, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IExpression, IExpressionContainer, ILaunch, IStackFrame, IThread, IViewModel, State } from './debug.js'; +import type { IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions } from './debugModel.js'; +import { DebugVisualizer, IDebugVisualizerService } from './debugVisualizers.js'; + +const nullViewModel: IViewModel = { + getId(): string { return 'root'; }, + focusedSession: undefined, + focusedThread: undefined, + focusedStackFrame: undefined, + setVisualizedExpression(): void { }, + getVisualizedExpression(): IExpression | string | undefined { return undefined; }, + getSelectedExpression(): undefined { return undefined; }, + setSelectedExpression(): void { }, + updateViews(): void { }, + isMultiSessionView(): boolean { return false; }, + onDidFocusSession: Event.None, + onDidFocusThread: Event.None, + onDidFocusStackFrame: Event.None, + onDidSelectExpression: Event.None, + onDidEvaluateLazyExpression: Event.None, + onDidChangeVisualization: Event.None, + onWillUpdateViews: Event.None, + evaluateLazyExpression(_expression: IExpressionContainer): void { }, +}; + +const nullDebugModel: IDebugModel = { + getId(): string { return 'root'; }, + getSession(): undefined { return undefined; }, + getSessions(): IDebugSession[] { return []; }, + getBreakpoints(): readonly IBreakpoint[] { return []; }, + areBreakpointsActivated(): boolean { return false; }, + getFunctionBreakpoints() { return []; }, + getDataBreakpoints() { return []; }, + getExceptionBreakpoints() { return []; }, + getExceptionBreakpointsForSession() { return []; }, + getInstructionBreakpoints() { return []; }, + getWatchExpressions() { return []; }, + registerBreakpointModes(): void { }, + getBreakpointModes() { return []; }, + onDidChangeBreakpoints: Event.None, + onDidChangeCallStack: Event.None, + onDidChangeWatchExpressions: Event.None, + onDidChangeWatchExpressionValue: Event.None, + async fetchCallstack(): Promise { }, +}; + +const nullConfigurationManager: IConfigurationManager = { + selectedConfiguration: { + launch: undefined, + getConfig: () => Promise.resolve(undefined), + name: undefined, + type: undefined, + }, + async selectConfiguration(): Promise { }, + getLaunches() { return []; }, + getLaunch() { return undefined; }, + getAllConfigurations() { return []; }, + removeRecentDynamicConfigurations(): void { }, + getRecentDynamicConfigurations() { return []; }, + onDidSelectConfiguration: Event.None, + onDidChangeConfigurationProviders: Event.None, + hasDebugConfigurationProvider(): boolean { return false; }, + async getDynamicProviders() { return []; }, + async getDynamicConfigurationsByType() { return []; }, + registerDebugConfigurationProvider() { return Disposable.None; }, + unregisterDebugConfigurationProvider(): void { }, + async resolveConfigurationByProviders() { return undefined; }, +}; + +const nullAdapterManager: IAdapterManager = { + onDidRegisterDebugger: Event.None, + hasEnabledDebuggers(): boolean { return false; }, + async getDebugAdapterDescriptor() { return undefined; }, + getDebuggerLabel() { return undefined; }, + someDebuggerInterestedInLanguage(): boolean { return false; }, + getDebugger() { return undefined; }, + async activateDebuggers(): Promise { }, + registerDebugAdapterFactory() { return Disposable.None; }, + createDebugAdapter() { return undefined; }, + registerDebugAdapterDescriptorFactory() { return Disposable.None; }, + unregisterDebugAdapterDescriptorFactory(): void { }, + async substituteVariables(_debugType: string, _folder: undefined, config: IConfig) { return config; }, + async runInTerminal() { return undefined; }, + getEnabledDebugger() { return undefined; }, + async guessDebugger() { return undefined; }, + get onDidDebuggersExtPointRead() { return Event.None; }, +}; + +export class NullDebugService implements IDebugService { + + declare readonly _serviceBrand: undefined; + + readonly state = State.Inactive; + readonly initializingOptions = undefined; + + readonly onDidChangeState = Event.None; + readonly onWillNewSession = Event.None; + readonly onDidNewSession = Event.None; + readonly onDidEndSession = Event.None; + + getConfigurationManager(): IConfigurationManager { return nullConfigurationManager; } + getAdapterManager(): IAdapterManager { return nullAdapterManager; } + getModel(): IDebugModel { return nullDebugModel; } + getViewModel(): IViewModel { return nullViewModel; } + + async focusStackFrame(_focusedStackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, _options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise { } + canSetBreakpointsIn(): boolean { return false; } + async addBreakpoints(): Promise { return []; } + async updateBreakpoints(): Promise { } + async enableOrDisableBreakpoints(_enable: boolean, _breakpoint?: IEnablement): Promise { } + async setBreakpointsActivated(_activated: boolean): Promise { } + async removeBreakpoints(_id?: string | string[]): Promise { } + addFunctionBreakpoint(_opts?: IFunctionBreakpointOptions, _id?: string): void { } + async updateFunctionBreakpoint(_id: string, _update: { name?: string; hitCondition?: string; condition?: string }): Promise { } + async removeFunctionBreakpoints(_id?: string): Promise { } + async addDataBreakpoint(_opts: IDataBreakpointOptions): Promise { } + async updateDataBreakpoint(_id: string, _update: { hitCondition?: string; condition?: string }): Promise { } + async removeDataBreakpoints(_id?: string): Promise { } + async addInstructionBreakpoint(_opts: IInstructionBreakpointOptions): Promise { } + async removeInstructionBreakpoints(_instructionReference?: string, _offset?: number, _address?: bigint): Promise { } + async setExceptionBreakpointCondition(_breakpoint: IExceptionBreakpoint, _condition: string | undefined): Promise { } + setExceptionBreakpointsForSession(_session: IDebugSession, _filters: DebugProtocol.ExceptionBreakpointsFilter[]): void { } + async sendAllBreakpoints(_session?: IDebugSession): Promise { } + async sendBreakpoints(_modelUri: URI, _sourceModified?: boolean, _session?: IDebugSession): Promise { } + addWatchExpression(_name?: string): void { } + renameWatchExpression(_id: string, _newName: string): void { } + moveWatchExpression(_id: string, _position: number): void { } + removeWatchExpressions(_id?: string): void { } + async startDebugging(_launch: ILaunch | undefined, _configOrName?: IConfig | string, _options?: IDebugSessionOptions, _saveBeforeStart?: boolean): Promise { return false; } + async restartSession(_session: IDebugSession, _restartData?: unknown): Promise { } + async stopSession(_session: IDebugSession | undefined, _disconnect?: boolean, _suspend?: boolean): Promise { } + sourceIsNotAvailable(): void { } + async runTo(): Promise { } +} + +export class NullDebugVisualizerService implements IDebugVisualizerService { + + declare readonly _serviceBrand: undefined; + + async getApplicableFor(): Promise> { return { object: [], dispose() { } }; } + register(): IDisposable { return Disposable.None; } + registerTree(): IDisposable { return Disposable.None; } + async getVisualizedNodeFor(): Promise { return undefined; } + async getVisualizedChildren(): Promise { return []; } + async editTreeItem(): Promise { } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index d1c8e7ea22b28..ca5f2458fb0b6 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -47,7 +47,6 @@ import { McpConfigMigrationContribution } from './mcpMigration.js'; import { McpResourceQuickAccess } from './mcpResourceQuickAccess.js'; import { McpServerEditor } from './mcpServerEditor.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; -import { McpServersViewsContribution } from './mcpServersView.js'; import { MCPContextsInitialisation, McpWorkbenchService } from './mcpWorkbenchService.js'; registerSingleton(IMcpRegistry, McpRegistry, InstantiationType.Delayed); @@ -101,7 +100,6 @@ registerWorkbenchContribution2('mcpActionRendering', MCPServerActionRendering, W registerWorkbenchContribution2('mcpAddContext', McpAddContextContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(MCPContextsInitialisation.ID, MCPContextsInitialisation, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(McpConfigMigrationContribution.ID, McpConfigMigrationContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(McpServersViewsContribution.ID, McpServersViewsContribution, WorkbenchPhase.AfterRestored); const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); jsonRegistry.registerSchema(mcpSchemaId, mcpServerSchema); diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.view.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.view.contribution.ts new file mode 100644 index 0000000000000..204f4682f1325 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcp.view.contribution.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { McpServersViewsContribution } from './mcpServersView.js'; + +registerWorkbenchContribution2(McpServersViewsContribution.ID, McpServersViewsContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index aff12a119b7d3..cd1acdfbb555f 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -55,8 +55,7 @@ import { IChatElicitationRequest, IChatToolInvocation } from '../../chat/common/ import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js'; import { ILanguageModelsService } from '../../chat/common/languageModels.js'; import { ILanguageModelToolsService } from '../../chat/common/tools/languageModelToolsService.js'; -import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js'; -import { extensionsFilterSubMenu, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { extensionsFilterSubMenu, IExtensionsWorkbenchService, VIEWLET_ID } from '../../extensions/common/extensions.js'; import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; import { McpCommandIds } from '../common/mcpCommandIds.js'; import { McpContextKeys } from '../common/mcpContextKeys.js'; @@ -940,7 +939,7 @@ export class ShowInstalledMcpServersCommand extends Action2 { const viewsService = accessor.get(IViewsService); const view = await viewsService.openView(InstalledMcpServersViewId, true); if (!view) { - await viewsService.openViewContainer(VIEW_CONTAINER.id); + await viewsService.openViewContainer(VIEWLET_ID); await viewsService.openView(InstalledMcpServersViewId, true); } } diff --git a/src/vs/workbench/contrib/search/browser/searchActionsBase.ts b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts index 7a180f06375a1..983270701ef45 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsBase.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts @@ -5,12 +5,22 @@ import * as DOM from '../../../../base/browser/dom.js'; import * as nls from '../../../../nls.js'; +import * as SearchEditorConstants from '../../searchEditor/browser/constants.js'; import { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { SearchView } from './searchView.js'; -import { ISearchConfigurationProperties, VIEW_ID } from '../../../services/search/common/search.js'; +import { ISearchConfiguration, ISearchConfigurationProperties, VIEW_ID } from '../../../services/search/common/search.js'; import { isSearchTreeMatch, RenderableMatch, ISearchResult, isSearchTreeFileMatch, isSearchTreeFolderMatch } from './searchTreeModel/searchTreeCommon.js'; import { searchComparer } from './searchCompare.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; +import { IHistoryService } from '../../../services/history/common/history.js'; +import { OpenSearchEditorArgs } from '../../searchEditor/browser/searchEditor.contribution.js'; +import { Schemas } from '../../../../base/common/network.js'; + export const category = nls.localize2('search', "Search"); @@ -23,8 +33,8 @@ export function getSearchView(viewsService: IViewsService): SearchView | undefin return viewsService.getActiveViewWithId(VIEW_ID) as SearchView; } -export function getElementsToOperateOn(viewer: WorkbenchCompressibleAsyncDataTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): RenderableMatch[] { - let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder)); +export function getElementsToOperateOn(viewer: WorkbenchCompressibleAsyncDataTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties | undefined): RenderableMatch[] { + let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig?.sortOrder)); // if selection doesn't include multiple elements, just return current focus element. if (currElement && !(elements.length > 1 && elements.includes(currElement))) { @@ -60,6 +70,82 @@ function hasDownstreamMatch(elements: RenderableMatch[], focusElement: Renderabl } +export interface IFindInFilesArgs { + query?: string; + replace?: string; + preserveCase?: boolean; + triggerSearch?: boolean; + filesToInclude?: string; + filesToExclude?: string; + isRegex?: boolean; + isCaseSensitive?: boolean; + matchWholeWord?: boolean; + useExcludeSettingsAndIgnoreFiles?: boolean; + onlyOpenEditors?: boolean; + showIncludesExcludes?: boolean; +} + export function openSearchView(viewsService: IViewsService, focus?: boolean): Promise { return viewsService.openView(VIEW_ID, focus).then(view => (view as SearchView ?? undefined)); } + +export async function findInFilesCommand(accessor: ServicesAccessor, _args: IFindInFilesArgs = {}) { + + const searchConfig = accessor.get(IConfigurationService).getValue().search; + const viewsService = accessor.get(IViewsService); + const commandService = accessor.get(ICommandService); + const args: IFindInFilesArgs = {}; + if (Object.keys(_args).length !== 0) { + // resolve variables in the same way as in + // https://github.com/microsoft/vscode/blob/8b76efe9d317d50cb5b57a7658e09ce6ebffaf36/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts#L152-L158 + const configurationResolverService = accessor.get(IConfigurationResolverService); + const historyService = accessor.get(IHistoryService); + const workspaceContextService = accessor.get(IWorkspaceContextService); + const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot(); + const filteredActiveWorkspaceRootUri = activeWorkspaceRootUri?.scheme === Schemas.file || activeWorkspaceRootUri?.scheme === Schemas.vscodeRemote ? activeWorkspaceRootUri : undefined; + const lastActiveWorkspaceRoot = filteredActiveWorkspaceRootUri ? workspaceContextService.getWorkspaceFolder(filteredActiveWorkspaceRootUri) ?? undefined : undefined; + + for (const entry of Object.entries(_args)) { + const name = entry[0]; + const value = entry[1]; + if (value !== undefined) { + // eslint-disable-next-line local/code-no-any-casts + (args as any)[name as any] = (typeof value === 'string') ? await configurationResolverService.resolveAsync(lastActiveWorkspaceRoot, value) : value; + } + } + } + + const mode = searchConfig?.mode; + if (mode === 'view') { + openSearchView(viewsService, false).then(openedView => { + if (openedView) { + const searchAndReplaceWidget = openedView.searchAndReplaceWidget; + searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string'); + let updatedText = false; + if (typeof args.query !== 'string') { + updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); + } + openedView.setSearchParameters(args); + if (typeof args.showIncludesExcludes === 'boolean') { + openedView.toggleQueryDetails(false, args.showIncludesExcludes); + } + + openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); + } + }); + } else { + const convertArgs = (args: IFindInFilesArgs): OpenSearchEditorArgs => ({ + location: mode === 'newEditor' ? 'new' : 'reuse', + query: args.query, + filesToInclude: args.filesToInclude, + filesToExclude: args.filesToExclude, + matchWholeWord: args.matchWholeWord, + isCaseSensitive: args.isCaseSensitive, + isRegexp: args.isRegex, + useExcludeSettingsAndIgnoreFiles: args.useExcludeSettingsAndIgnoreFiles, + onlyOpenEditors: args.onlyOpenEditors, + showIncludesExcludes: !!(args.filesToExclude || args.filesToExclude || !args.useExcludeSettingsAndIgnoreFiles), + }); + commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, convertArgs(args)); + } +} diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts index 9f99aa2b4ed16..b72ff6d50ed2c 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -12,7 +12,6 @@ import { ViewContainerLocation } from '../../../common/views.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import * as Constants from '../common/constants.js'; import * as SearchEditorConstants from '../../searchEditor/browser/constants.js'; -import { OpenSearchEditorArgs } from '../../searchEditor/browser/searchEditor.contribution.js'; import { ISearchConfiguration, ISearchConfigurationProperties } from '../../../services/search/common/search.js'; import { URI } from '../../../../base/common/uri.js'; import { IsSessionsWindowContext } from '../../../common/contextkeys.js'; @@ -28,32 +27,12 @@ import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { ExplorerViewPaneContainer } from '../../files/browser/explorerViewlet.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { category, getElementsToOperateOn, getSearchView, openSearchView } from './searchActionsBase.js'; -import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; -import { IHistoryService } from '../../../services/history/common/history.js'; -import { Schemas } from '../../../../base/common/network.js'; +import { category, findInFilesCommand, getElementsToOperateOn, getSearchView, IFindInFilesArgs, openSearchView } from './searchActionsBase.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { forcedExpandRecursively } from './searchActionsTopBar.js'; import { RenderableMatch, ISearchTreeFileMatch, ISearchTreeFolderMatchWithResource, ISearchResult, isSearchTreeFileMatch, isSearchTreeMatch } from './searchTreeModel/searchTreeCommon.js'; -//#region Interfaces -export interface IFindInFilesArgs { - query?: string; - replace?: string; - preserveCase?: boolean; - triggerSearch?: boolean; - filesToInclude?: string; - filesToExclude?: string; - isRegex?: boolean; - isCaseSensitive?: boolean; - matchWholeWord?: boolean; - useExcludeSettingsAndIgnoreFiles?: boolean; - onlyOpenEditors?: boolean; - showIncludesExcludes?: boolean; -} -//#endregion - registerAction2(class RestrictSearchToFolderAction extends Action2 { constructor() { super({ @@ -325,7 +304,7 @@ registerAction2(class FindInWorkspaceAction extends Action2 { } async run(accessor: ServicesAccessor) { const searchConfig = accessor.get(IConfigurationService).getValue().search; - const mode = searchConfig.mode; + const mode = searchConfig?.mode; if (mode === 'view') { const searchView = await openSearchView(accessor.get(IViewsService), true); @@ -382,7 +361,7 @@ async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplore const contextService = accessor.get(IWorkspaceContextService); const commandService = accessor.get(ICommandService); const searchConfig = accessor.get(IConfigurationService).getValue().search; - const mode = searchConfig.mode; + const mode = searchConfig?.mode; let resources: URI[]; @@ -434,73 +413,12 @@ async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplore } } -function getMultiSelectedSearchResources(viewer: WorkbenchCompressibleAsyncDataTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): URI[] { +function getMultiSelectedSearchResources(viewer: WorkbenchCompressibleAsyncDataTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties | undefined): URI[] { return getElementsToOperateOn(viewer, currElement, sortConfig) .map((renderableMatch) => ((isSearchTreeMatch(renderableMatch)) ? null : renderableMatch.resource)) .filter((renderableMatch): renderableMatch is URI => (renderableMatch !== null)); } -export async function findInFilesCommand(accessor: ServicesAccessor, _args: IFindInFilesArgs = {}) { - - const searchConfig = accessor.get(IConfigurationService).getValue().search; - const viewsService = accessor.get(IViewsService); - const commandService = accessor.get(ICommandService); - const args: IFindInFilesArgs = {}; - if (Object.keys(_args).length !== 0) { - // resolve variables in the same way as in - // https://github.com/microsoft/vscode/blob/8b76efe9d317d50cb5b57a7658e09ce6ebffaf36/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts#L152-L158 - const configurationResolverService = accessor.get(IConfigurationResolverService); - const historyService = accessor.get(IHistoryService); - const workspaceContextService = accessor.get(IWorkspaceContextService); - const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot(); - const filteredActiveWorkspaceRootUri = activeWorkspaceRootUri?.scheme === Schemas.file || activeWorkspaceRootUri?.scheme === Schemas.vscodeRemote ? activeWorkspaceRootUri : undefined; - const lastActiveWorkspaceRoot = filteredActiveWorkspaceRootUri ? workspaceContextService.getWorkspaceFolder(filteredActiveWorkspaceRootUri) ?? undefined : undefined; - - for (const entry of Object.entries(_args)) { - const name = entry[0]; - const value = entry[1]; - if (value !== undefined) { - // eslint-disable-next-line local/code-no-any-casts - (args as any)[name as any] = (typeof value === 'string') ? await configurationResolverService.resolveAsync(lastActiveWorkspaceRoot, value) : value; - } - } - } - - const mode = searchConfig.mode; - if (mode === 'view') { - openSearchView(viewsService, false).then(openedView => { - if (openedView) { - const searchAndReplaceWidget = openedView.searchAndReplaceWidget; - searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string'); - let updatedText = false; - if (typeof args.query !== 'string') { - updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); - } - openedView.setSearchParameters(args); - if (typeof args.showIncludesExcludes === 'boolean') { - openedView.toggleQueryDetails(false, args.showIncludesExcludes); - } - - openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); - } - }); - } else { - const convertArgs = (args: IFindInFilesArgs): OpenSearchEditorArgs => ({ - location: mode === 'newEditor' ? 'new' : 'reuse', - query: args.query, - filesToInclude: args.filesToInclude, - filesToExclude: args.filesToExclude, - matchWholeWord: args.matchWholeWord, - isCaseSensitive: args.isCaseSensitive, - isRegexp: args.isRegex, - useExcludeSettingsAndIgnoreFiles: args.useExcludeSettingsAndIgnoreFiles, - onlyOpenEditors: args.onlyOpenEditors, - showIncludesExcludes: !!(args.filesToExclude || args.filesToExclude || !args.useExcludeSettingsAndIgnoreFiles), - }); - commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, convertArgs(args)); - } -} - async function modifySearchFileTypePattern(accessor: ServicesAccessor, fileMatch: ISearchTreeFileMatch | undefined, isExclude: boolean) { const viewsService = accessor.get(IViewsService); const searchView = getSearchView(viewsService); diff --git a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts index 3dff8656292f2..cae7e01cd2b29 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts @@ -308,7 +308,7 @@ async function performReplace(accessor: ServicesAccessor, viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); if (isSearchTreeMatch(nextFocusElement)) { - const useReplacePreview = configurationService.getValue().search.useReplacePreview; + const useReplacePreview = configurationService.getValue().search?.useReplacePreview; if (!useReplacePreview || instantiationService.invokeFunction(accessor => hasToOpenFile(accessor, nextFocusElement!)) || nextFocusElement instanceof MatchInNotebook) { viewlet?.open(nextFocusElement, true); } else { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index fb52bbb7553fd..d277827191677 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -57,7 +57,7 @@ import { Memento } from '../../../common/memento.js'; import { IViewDescriptorService } from '../../../common/views.js'; import { NotebookEditor } from '../../notebook/browser/notebookEditor.js'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from './patternInputWidget.js'; -import { IFindInFilesArgs } from './searchActionsFind.js'; +import { IFindInFilesArgs } from './searchActionsBase.js'; import { searchDetailsIcon } from './searchIcons.js'; import { renderSearchMessage } from './searchMessage.js'; import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate, TextSearchResultRenderer } from './searchResultsView.js'; @@ -2168,7 +2168,7 @@ export class SearchView extends ViewPane { // clean up ui // this.replaceService.disposeAllReplacePreviews(); - if (showingCancelled || forceHideMessages || !this.configurationService.getValue().search.searchOnType) { + if (showingCancelled || forceHideMessages || !this.configurationService.getValue().search?.searchOnType) { // when in search to type, don't preemptively hide, as it causes flickering and shifting of the live results dom.hide(this.messagesElement); } @@ -2184,7 +2184,7 @@ export class SearchView extends ViewPane { } private onFocus(lineMatch: ISearchTreeMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { - const useReplacePreview = this.configurationService.getValue().search.useReplacePreview; + const useReplacePreview = this.configurationService.getValue().search?.useReplacePreview; const resource = isSearchTreeMatch(lineMatch) ? lineMatch.parent().resource : (lineMatch).resource; return (useReplacePreview && this.viewModel.isReplaceActive() && !!this.viewModel.replaceString && !(this.shouldOpenInNotebookEditor(lineMatch, resource))) ? diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index c8859acb39ab4..e056c22a1c3d8 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './tabCompletion.js'; +import './snippets.service.contribution.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../../base/common/jsonSchema.js'; import * as nls from '../../../../nls.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import * as JSONContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js'; @@ -16,16 +17,10 @@ import { ApplyFileSnippetAction } from './commands/fileTemplateSnippets.js'; import { InsertSnippetAction } from './commands/insertSnippet.js'; import { SurroundWithSnippetEditorAction } from './commands/surroundWithSnippet.js'; import { SnippetCodeActions } from './snippetCodeActionProvider.js'; -import { ISnippetsService } from './snippets.js'; -import { SnippetsService } from './snippetsService.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; - -import './tabCompletion.js'; import { editorConfigurationBaseNode } from '../../../../editor/common/config/editorConfigurationSchema.js'; -// service -registerSingleton(ISnippetsService, SnippetsService, InstantiationType.Delayed); // actions registerAction2(InsertSnippetAction); diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.service.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.service.contribution.ts new file mode 100644 index 0000000000000..a59b3eec65808 --- /dev/null +++ b/src/vs/workbench/contrib/snippets/browser/snippets.service.contribution.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { ISnippetsService } from './snippets.js'; +import { SnippetsService } from './snippetsService.js'; + +registerSingleton(ISnippetsService, SnippetsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution.ts b/src/vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution.ts index f6c3e79b229bc..06fedf27f2625 100644 --- a/src/vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution.ts @@ -12,7 +12,7 @@ import { localize2 } from '../../../../../nls.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { findInFilesCommand } from '../../../search/browser/searchActionsFind.js'; +import { findInFilesCommand } from '../../../search/browser/searchActionsBase.js'; import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from '../../../terminal/browser/terminal.js'; import { registerActiveInstanceAction, registerActiveXtermAction } from '../../../terminal/browser/terminalActions.js'; import { registerTerminalContribution, type IDetachedCompatibleTerminalContributionContext, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; diff --git a/src/vs/workbench/services/search/common/queryBuilder.ts b/src/vs/workbench/services/search/common/queryBuilder.ts index e2beb5012b69b..1e14a1838949f 100644 --- a/src/vs/workbench/services/search/common/queryBuilder.ts +++ b/src/vs/workbench/services/search/common/queryBuilder.ts @@ -144,7 +144,7 @@ export class QueryBuilder { const fallbackToPCRE = folderResources && folderResources.some(folder => { const folderConfig = this.configurationService.getValue({ resource: folder }); - return !folderConfig.search.useRipgrep; + return !folderConfig.search?.useRipgrep; }); const commonQuery = this.commonQuery(folderResources?.map(toWorkspaceFolder), options); @@ -154,7 +154,7 @@ export class QueryBuilder { contentPattern, previewOptions: options.previewOptions, maxFileSize: options.maxFileSize, - usePCRE2: searchConfig.search.usePCRE2 || fallbackToPCRE || false, + usePCRE2: searchConfig.search?.usePCRE2 || fallbackToPCRE || false, surroundingContext: options.surroundingContext, userDisabledExcludesAndIgnoreFiles: options.disregardExcludeSettings && options.disregardIgnoreFiles, @@ -615,10 +615,10 @@ export class QueryBuilder { folderName: includeFolderName ? folderName : undefined, excludePattern: excludePatternRet, fileEncoding: folderConfig.files && folderConfig.files.encoding, - disregardIgnoreFiles: typeof options.disregardIgnoreFiles === 'boolean' ? options.disregardIgnoreFiles : !folderConfig.search.useIgnoreFiles, - disregardGlobalIgnoreFiles: typeof options.disregardGlobalIgnoreFiles === 'boolean' ? options.disregardGlobalIgnoreFiles : !folderConfig.search.useGlobalIgnoreFiles, - disregardParentIgnoreFiles: typeof options.disregardParentIgnoreFiles === 'boolean' ? options.disregardParentIgnoreFiles : !folderConfig.search.useParentIgnoreFiles, - ignoreSymlinks: typeof options.ignoreSymlinks === 'boolean' ? options.ignoreSymlinks : !folderConfig.search.followSymlinks, + disregardIgnoreFiles: typeof options.disregardIgnoreFiles === 'boolean' ? options.disregardIgnoreFiles : !folderConfig.search?.useIgnoreFiles, + disregardGlobalIgnoreFiles: typeof options.disregardGlobalIgnoreFiles === 'boolean' ? options.disregardGlobalIgnoreFiles : !folderConfig.search?.useGlobalIgnoreFiles, + disregardParentIgnoreFiles: typeof options.disregardParentIgnoreFiles === 'boolean' ? options.disregardParentIgnoreFiles : !folderConfig.search?.useParentIgnoreFiles, + ignoreSymlinks: typeof options.ignoreSymlinks === 'boolean' ? options.ignoreSymlinks : !folderConfig.search?.followSymlinks, ignoreGlobCase: options.ignoreGlobCase, }; } diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index b425f20be3e03..9d37c49ad7f18 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -480,7 +480,7 @@ export interface ISearchConfigurationProperties { } export interface ISearchConfiguration extends IFilesConfiguration { - search: ISearchConfigurationProperties; + search?: ISearchConfigurationProperties; editor: { wordSeparators: string; }; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 4c06830a7f574..dd3b5ec6fd273 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -212,8 +212,10 @@ import './contrib/speech/browser/speech.contribution.js'; // Chat import './contrib/chat/browser/chat.contribution.js'; +import './contrib/chat/browser/chat.view.contribution.js'; import './contrib/inlineChat/browser/inlineChat.contribution.js'; import './contrib/mcp/browser/mcp.contribution.js'; +import './contrib/mcp/browser/mcp.view.contribution.js'; import './contrib/chat/browser/chatSessions/chatSessions.contribution.js'; import './contrib/chat/browser/contextContrib/chatContext.contribution.js'; import './contrib/imageCarousel/browser/imageCarousel.contribution.js'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 8ecb4d298e49c..4e4b802d3e16d 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -177,8 +177,6 @@ import './contrib/multiDiffEditor/browser/multiDiffEditor.contribution.js'; // Remote Tunnel import './contrib/remoteTunnel/electron-browser/remoteTunnel.contribution.js'; -// Chat -import './contrib/chat/electron-browser/chat.contribution.js'; // Encryption import './contrib/encryption/electron-browser/encryption.contribution.js';