diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 080d97f3197cf..1e216b7f5ad92 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -97,6 +97,7 @@ const vscodeResourceIncludes = [ // Welcome 'out-build/vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.{svg,png}', + 'out-build/vs/workbench/contrib/welcomeOnboarding/browser/media/*.svg', // Sessions 'out-build/vs/sessions/contrib/chat/browser/media/*.svg', diff --git a/build/gulpfile.vscode.web.ts b/build/gulpfile.vscode.web.ts index 3e6b29adfe9fa..9af2afecb38ba 100644 --- a/build/gulpfile.vscode.web.ts +++ b/build/gulpfile.vscode.web.ts @@ -74,6 +74,7 @@ export const vscodeWebResourceIncludes = [ // Welcome 'out-build/vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.{svg,png}', + 'out-build/vs/workbench/contrib/welcomeOnboarding/browser/media/*.svg', // Extensions 'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', diff --git a/cglicenses.json b/cglicenses.json index 19b217756dfc5..3dffb9585b724 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -830,5 +830,111 @@ "", "THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." ] + }, + { + "name": "@anthropic-ai/claude-agent-sdk", + "licenseDetail": [ + "© Anthropic PBC. All rights reserved. Use is subject to Anthropic's Commercial Terms of Service." + ] + }, + { + "name": "@github/blackbird-external-ingest-utils", + "licenseDetail": [ + "MIT License" + ] + }, + { + "name": "@microsoft/dev-tunnels-connections", + "licenseDetail": [ + "MIT License" + ] + }, + { + "name": "@microsoft/dev-tunnels-contracts", + "licenseDetail": [ + "MIT License" + ] + }, + { + "name": "@microsoft/dev-tunnels-management", + "licenseDetail": [ + "MIT License" + ] + }, + { + "name": "brorand", + "licenseDetail": [ + "This software is licensed under the MIT License.", + "", + "Copyright Fedor Indutny, 2014.", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] + }, + { + "name": "emitter-listener", + "licenseDetail": [ + "BSD-2-Clause" + ] + }, + { + "name": "bignumber.js", + "licenseDetail": [ + "Copyright © <2026> Michael Mclaughlin", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] + }, + { + "name": "miller-rabin", + "licenseDetail": [ + "This software is licensed under the MIT License.", + "", + "Copyright Fedor Indutny, 2014.", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] + }, + { + "name": "gcp-metadata", + "fullLicenseTextUri": "https://github.com/googleapis/google-cloud-node-core/blob/76ba85a7f55c6e82943008b4eceb07a0f58b39e1/LICENSE" + }, + { + "name": "randombytes", + "licenseDetail": [ + "MIT License", + "", + "Copyright (c) 2017 crypto-browserify", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ] } ] diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index b00e2f1d0a6b9..74e73d9ca76d3 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -4435,6 +4435,7 @@ "number", "null" ], + "default": 10, "markdownDescription": "%github.copilot.config.inlineEdits.triggerOnEditorChangeAfterSeconds%", "tags": [ "advanced", diff --git a/extensions/copilot/src/extension/inlineEdits/test/vscode-node/inlineEditTriggerer.spec.ts b/extensions/copilot/src/extension/inlineEdits/test/vscode-node/inlineEditTriggerer.spec.ts index 2193f7f6ee84e..972253a394ff0 100644 --- a/extensions/copilot/src/extension/inlineEdits/test/vscode-node/inlineEditTriggerer.spec.ts +++ b/extensions/copilot/src/extension/inlineEdits/test/vscode-node/inlineEditTriggerer.spec.ts @@ -392,6 +392,7 @@ suite('InlineEditTriggerer', () => { const doc2 = createTextDocument(undefined, Uri.file('file2.py')); nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1; + nextEditProvider.lastOutcome = NesOutcome.Accepted; // Configure to trigger on document switch void configurationService.setConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, 30); @@ -504,6 +505,7 @@ suite('InlineEditTriggerer', () => { const doc2 = createTextDocument(undefined, Uri.file('file2.py')); nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1; + nextEditProvider.lastOutcome = NesOutcome.Accepted; const triggerAfterSeconds = 30; // Configure to trigger on document switch @@ -930,6 +932,7 @@ suite('InlineEditTriggerer', () => { const doc2 = createTextDocument(undefined, Uri.file('file2.py'), 'line1\nline2'); nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1; + nextEditProvider.lastOutcome = NesOutcome.Accepted; void configurationService.setConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, 30); // Edit doc1 and trigger @@ -1148,6 +1151,7 @@ suite('InlineEditTriggerer', () => { const doc2 = createTextDocument(undefined, Uri.file('file2.py')); nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1; + nextEditProvider.lastOutcome = NesOutcome.Accepted; void configurationService.setConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, 30); // Fire an undo change — this should still update lastEditTimestamp @@ -1216,25 +1220,31 @@ suite('InlineEditTriggerer', () => { assert.strictEqual(firedEvents.length, 0, 'Should not fire on doc switch during rejection cooldown'); }); - test('Same-line cooldown is bypassed when triggerOnActiveEditorChange is set', () => { - const { document, textEditor } = createTextDocument(); + test('Same-line cooldown is bypassed after switching away and back', () => { + const doc1 = createTextDocument(undefined, Uri.file('file1.py')); + const doc2 = createTextDocument(undefined, Uri.file('file2.py')); nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1; - // Enable triggerOnActiveEditorChange — this bypasses same-line cooldown - void configurationService.setConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, 30); - - triggerTextChange(document); - triggerTextSelectionChange(textEditor, new Selection(0, 5, 0, 5)); + // Edit doc1 and trigger on line 0 + triggerTextChange(doc1.document); + triggerTextSelectionChange(doc1.textEditor, new Selection(0, 5, 0, 5)); const initialCount = firedEvents.length; assert.isAtLeast(initialCount, 1, 'First trigger should fire'); - // Same line, different column — normally would be in cooldown, - // but triggerOnActiveEditorChange is set so cooldown is bypassed - triggerTextSelectionChange(textEditor, new Selection(0, 10, 0, 10)); + // Same line — cooldown blocks + triggerTextSelectionChange(doc1.textEditor, new Selection(0, 10, 0, 10)); + assert.strictEqual(firedEvents.length, initialCount, 'Same-line cooldown should block'); + + // Switch to doc2 + triggerTextChange(doc2.document); + triggerTextSelectionChange(doc2.textEditor, new Selection(0, 0, 0, 0)); + const countAfterDoc2 = firedEvents.length; - assert.isAtLeast(firedEvents.length, initialCount + 1, - 'Same-line cooldown should be bypassed when triggerOnActiveEditorChange is set'); + // Switch back to doc1, same line — cooldown should be cleared by the doc switch + triggerTextSelectionChange(doc1.textEditor, new Selection(0, 10, 0, 10)); + assert.isAtLeast(firedEvents.length, countAfterDoc2 + 1, + 'Same-line cooldown should be bypassed after switching away and back'); }); test('Output pane documents are ignored for selection changes', () => { diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineEditTriggerer.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineEditTriggerer.ts index 9eaf836c1d3f9..b50da76660495 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineEditTriggerer.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineEditTriggerer.ts @@ -157,6 +157,12 @@ export class InlineEditTriggerer extends Disposable { return; } + // When the user switches to a different file and comes back, clear same-line + // cooldowns so the triggerer fires again on the same line. + if (!isSameDoc) { + mostRecentChange.lineNumberTriggers.clear(); + } + const hadRecentEdit = this._hasRecentEdit(mostRecentChange); if (!hadRecentEdit || !this._hasRecentTrigger()) { // The edit is too old or the provider was not triggered recently (we might be @@ -221,17 +227,14 @@ export class InlineEditTriggerer extends Disposable { /** * Returns true if the same-line cooldown is active and we should skip triggering. * - * The cooldown is bypassed when: - * - `triggerOnActiveEditorChange` is configured, OR - * - we're in a notebook cell and the current document differs from the one that - * originally triggered the change (user moved to a different cell). + * The cooldown is bypassed when we're in a notebook cell and the current document + * differs from the one that originally triggered the change (user moved to a + * different cell). + * + * When the user switches to a different file and comes back, line triggers are + * cleared (see {@link _handleSelectionChange}), so the cooldown naturally resets. */ private _isSameLineCooldownActive(mostRecentChange: LastChange, selectionLine: number, currentDocument: vscode.TextDocument): boolean { - const triggerOnActiveEditorChange = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, this._expService); - if (triggerOnActiveEditorChange) { - return false; // cooldown bypassed - } - // In a notebook, if the user moved to a different cell, bypass the cooldown if (isNotebookCell(currentDocument.uri) && currentDocument !== mostRecentChange.documentTrigger) { return false; // cooldown bypassed diff --git a/extensions/copilot/src/platform/chat/common/commonTypes.ts b/extensions/copilot/src/platform/chat/common/commonTypes.ts index c869cb511073a..fad49df32483f 100644 --- a/extensions/copilot/src/platform/chat/common/commonTypes.ts +++ b/extensions/copilot/src/platform/chat/common/commonTypes.ts @@ -198,7 +198,7 @@ export type ChatResponse = FetchResponse; export type ChatResponses = FetchResponse; -function getRateLimitMessage(fetchResult: ChatFetchError): string { +function getRateLimitMessage(fetchResult: ChatFetchError, copilotPlan: string | undefined): string { if (fetchResult.type !== ChatFetchResponseType.RateLimited) { throw new Error('Expected RateLimited error'); } @@ -225,8 +225,41 @@ function getRateLimitMessage(fetchResult: ChatFetchError): string { }); } if (fetchResult.capiError?.code?.startsWith('user_global_rate_limited')) { + if (copilotPlan === 'free' || copilotPlan === 'individual' || copilotPlan === 'individual_pro') { + return l10n.t({ + message: 'You\'ve hit your global rate limit. Please upgrade your plan or wait {0} for your limit to reset. [Learn More]({1})', + args: [retryAfterString, 'https://aka.ms/github-copilot-rate-limit-error'], + comment: [`{Locked=']({'}`] + }); + } + + return l10n.t({ + message: 'You\'ve hit your global rate limit. Please wait {0} for your limit to reset. [Learn More]({1})', + args: [retryAfterString, 'https://aka.ms/github-copilot-rate-limit-error'], + comment: [`{Locked=']({'}`] + }); + } + if (fetchResult.capiError?.code?.startsWith('user_weekly_rate_limited')) { + if (fetchResult.retryAfter) { + const resetDate = new Date(Date.now() + fetchResult.retryAfter * 1000); + const resetDateString = resetDate.toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit' }); + if (copilotPlan === 'free' || copilotPlan === 'individual' || copilotPlan === 'individual_pro') { + return l10n.t({ + message: 'You\'ve reached your weekly rate limit. Please upgrade your plan or wait for your limit to reset on {0}. [Learn More]({1})', + args: [resetDateString, 'https://aka.ms/github-copilot-rate-limit-error'], + comment: [`{Locked=']({'}`] + }); + } + + return l10n.t({ + message: 'You\'ve reached your weekly rate limit. Please wait for your limit to reset on {0}. [Learn More]({1})', + args: [resetDateString, 'https://aka.ms/github-copilot-rate-limit-error'], + comment: [`{Locked=']({'}`] + }); + } + return l10n.t({ - message: 'You\'ve hit your global rate limit. Please upgrade your plan or wait {0} for your limit to reset. [Learn More]({1})', + message: 'You\'ve reached your weekly rate limit. Please wait {0} for your limit to reset. [Learn More]({1})', args: [retryAfterString, 'https://aka.ms/github-copilot-rate-limit-error'], comment: [`{Locked=']({'}`] }); @@ -332,7 +365,7 @@ function getErrorDetailsFromChatFetchErrorInner(fetchResult: ChatFetchError, cop break; case ChatFetchResponseType.RateLimited: details = { - message: getRateLimitMessage(fetchResult), + message: getRateLimitMessage(fetchResult, copilotPlan), level: ChatErrorLevel.Info, isRateLimited: true }; diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 63cdf7c02f8f1..e2fc192a2304e 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -659,7 +659,7 @@ export namespace ConfigKey { /** Maximum number of tool calls the execution subagent can make */ export const ExecutionSubagentToolCallLimit = defineSetting('chat.executionSubagent.toolCallLimit', ConfigType.ExperimentBased, 10); - export const InlineEditsTriggerOnEditorChangeAfterSeconds = defineAndMigrateExpSetting('chat.advanced.inlineEdits.triggerOnEditorChangeAfterSeconds', 'chat.inlineEdits.triggerOnEditorChangeAfterSeconds', undefined); + export const InlineEditsTriggerOnEditorChangeAfterSeconds = defineAndMigrateExpSetting('chat.advanced.inlineEdits.triggerOnEditorChangeAfterSeconds', 'chat.inlineEdits.triggerOnEditorChangeAfterSeconds', 10); export const InlineEditsNextCursorPredictionDisplayLine = defineAndMigrateExpSetting('chat.advanced.inlineEdits.nextCursorPrediction.displayLine', 'chat.inlineEdits.nextCursorPrediction.displayLine', true); export const InlineEditsNextCursorPredictionCurrentFileMaxTokens = defineAndMigrateExpSetting('chat.advanced.inlineEdits.nextCursorPrediction.currentFileMaxTokens', 'chat.inlineEdits.nextCursorPrediction.currentFileMaxTokens', 3000); export const InlineEditsRenameSymbolSuggestions = defineSetting('chat.inlineEdits.renameSymbolSuggestions', ConfigType.ExperimentBased, true); @@ -777,7 +777,7 @@ export namespace ConfigKey { export const InlineEditsSpeculativeRequestsAutoExpandEditWindowLines = defineTeamInternalSetting('chat.advanced.inlineEdits.speculativeRequestsAutoExpandEditWindowLines', ConfigType.ExperimentBased, SpeculativeRequestsAutoExpandEditWindowLines.Off, SpeculativeRequestsAutoExpandEditWindowLines.VALIDATOR); export const InlineEditsExtraDebounceInlineSuggestion = defineTeamInternalSetting('chat.advanced.inlineEdits.extraDebounceInlineSuggestion', ConfigType.ExperimentBased, 0); export const InlineEditsDebounceOnSelectionChange = defineTeamInternalSetting('chat.advanced.inlineEdits.debounceOnSelectionChange', ConfigType.ExperimentBased, undefined); - export const InlineEditsTriggerOnEditorChangeStrategy = defineTeamInternalSetting('chat.advanced.inlineEdits.triggerOnEditorChangeStrategy', ConfigType.ExperimentBased, triggerOptions.DocumentSwitchTriggerStrategy.Always, triggerOptions.DocumentSwitchTriggerStrategy.VALIDATOR); + export const InlineEditsTriggerOnEditorChangeStrategy = defineTeamInternalSetting('chat.advanced.inlineEdits.triggerOnEditorChangeStrategy', ConfigType.ExperimentBased, triggerOptions.DocumentSwitchTriggerStrategy.AfterAcceptance, triggerOptions.DocumentSwitchTriggerStrategy.VALIDATOR); export const InlineEditsProviderId = defineTeamInternalSetting('chat.advanced.inlineEdits.providerId', ConfigType.ExperimentBased, undefined); export const InlineEditsUnification = defineTeamInternalSetting('chat.advanced.inlineEdits.unification', ConfigType.ExperimentBased, false); export const InlineEditsNextCursorPredictionModelName = defineTeamInternalSetting('chat.advanced.inlineEdits.nextCursorPrediction.modelName', ConfigType.ExperimentBased, 'copilot-suggestions-himalia-001'); diff --git a/extensions/copilot/src/platform/endpoint/node/messagesApi.ts b/extensions/copilot/src/platform/endpoint/node/messagesApi.ts index 7e0ac181c2d5c..28095403bcdea 100644 --- a/extensions/copilot/src/platform/endpoint/node/messagesApi.ts +++ b/extensions/copilot/src/platform/endpoint/node/messagesApi.ts @@ -142,12 +142,12 @@ export function createMessagesRequestBody(accessor: ServicesAccessor, options: I // is configured for the model, and the model supports thinking. reasoningEffort (if present) // is used only to configure the effort level when thinking is enabled, not to gate it. const reasoningEffort = options.modelCapabilities?.reasoningEffort; - let thinkingConfig: { type: 'enabled' | 'adaptive'; budget_tokens?: number } | undefined; + let thinkingConfig: { type: 'enabled' | 'adaptive'; budget_tokens?: number; display?: 'summarized' } | undefined; if (options.modelCapabilities?.enableThinking) { const configuredBudget = configurationService.getConfig(ConfigKey.AnthropicThinkingBudget); const thinkingExplicitlyDisabled = configuredBudget === 0; if (endpoint.supportsAdaptiveThinking && !thinkingExplicitlyDisabled) { - thinkingConfig = { type: 'adaptive' }; + thinkingConfig = { type: 'adaptive', display: 'summarized' }; } else if (!thinkingExplicitlyDisabled && endpoint.maxThinkingBudget && endpoint.minThinkingBudget) { const maxTokens = options.postOptions.max_tokens ?? 1024; const minBudget = endpoint.minThinkingBudget ?? 1024; @@ -225,7 +225,6 @@ export function createMessagesRequestBody(accessor: ServicesAccessor, options: I ...messagesResult, stream: true, tools: finalTools.length > 0 ? finalTools : undefined, - top_p: options.postOptions.top_p, max_tokens: options.postOptions.max_tokens, thinking: thinkingConfig, ...(effort ? { output_config: { effort } } : {}), diff --git a/extensions/copilot/src/platform/endpoint/test/node/messagesApi.spec.ts b/extensions/copilot/src/platform/endpoint/test/node/messagesApi.spec.ts index 4a53bdbe8eb1e..da00bbb684bd2 100644 --- a/extensions/copilot/src/platform/endpoint/test/node/messagesApi.spec.ts +++ b/extensions/copilot/src/platform/endpoint/test/node/messagesApi.spec.ts @@ -737,7 +737,7 @@ describe('createMessagesRequestBody reasoning effort', () => { const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint); - expect(body.thinking).toEqual({ type: 'adaptive' }); + expect(body.thinking).toEqual({ type: 'adaptive', display: 'summarized' }); expect(body.output_config).toEqual({ effort: 'high' }); }); @@ -752,7 +752,7 @@ describe('createMessagesRequestBody reasoning effort', () => { const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint); - expect(body.thinking).toEqual({ type: 'adaptive' }); + expect(body.thinking).toEqual({ type: 'adaptive', display: 'summarized' }); expect(body.output_config).toBeUndefined(); }); @@ -767,7 +767,7 @@ describe('createMessagesRequestBody reasoning effort', () => { const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint); - expect(body.thinking).toEqual({ type: 'adaptive' }); + expect(body.thinking).toEqual({ type: 'adaptive', display: 'summarized' }); expect(body.output_config).toBeUndefined(); }); @@ -797,7 +797,7 @@ describe('createMessagesRequestBody reasoning effort', () => { const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint); - expect(body.thinking).toEqual({ type: 'adaptive' }); + expect(body.thinking).toEqual({ type: 'adaptive', display: 'summarized' }); expect(body.output_config).toBeUndefined(); }); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 170e64f301108..720e380f8c55b 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -70,7 +70,6 @@ import { IResolvedTextEditorModel, ITextModelService } from '../../../../../edit import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditorOptions.js'; import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js'; -import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; @@ -88,7 +87,6 @@ import { ICustomizationHarnessService, CustomizationHarness, matchesWorkspaceSub import { ChatConfiguration } from '../../common/constants.js'; import { AICustomizationWelcomePage } from './aiCustomizationWelcomePage.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { IChatWidgetService } from '../chat.js'; const $ = DOM.$; @@ -172,6 +170,7 @@ interface ISectionItem { readonly id: AICustomizationManagementSection; readonly label: string; readonly icon: ThemeIcon; + readonly description: string; count: number; } @@ -210,20 +209,25 @@ interface ISectionItemTemplateData { readonly icon: HTMLElement; readonly label: HTMLElement; readonly count: HTMLElement; + readonly templateDisposables: DisposableStore; } class SectionItemRenderer implements IListRenderer { readonly templateId = 'sectionItem'; + constructor(private readonly hoverService: IHoverService) { } + renderTemplate(container: HTMLElement): ISectionItemTemplateData { container.classList.add('section-list-item'); const icon = DOM.append(container, $('.section-icon')); const label = DOM.append(container, $('.section-label')); const count = DOM.append(container, $('.section-count')); - return { container, icon, label, count }; + const templateDisposables = new DisposableStore(); + return { container, icon, label, count, templateDisposables }; } renderElement(element: ISectionItem, index: number, templateData: ISectionItemTemplateData): void { + templateData.templateDisposables.clear(); templateData.icon.className = 'section-icon'; templateData.icon.classList.add(...ThemeIcon.asClassNameArray(element.icon)); templateData.label.textContent = element.label; @@ -234,9 +238,12 @@ class SectionItemRenderer implements IListRenderer this.disposeBuiltinEditingSessions())); // Build sections from the workspace service configuration - const sectionInfo: Record = { - [AICustomizationManagementSection.Agents]: { label: localize('agents', "Agents"), icon: agentIcon }, - [AICustomizationManagementSection.Skills]: { label: localize('skills', "Skills"), icon: skillIcon }, - [AICustomizationManagementSection.Instructions]: { label: localize('instructions', "Instructions"), icon: instructionsIcon }, - [AICustomizationManagementSection.Prompts]: { label: localize('prompts', "Prompts"), icon: promptIcon }, - [AICustomizationManagementSection.Hooks]: { label: localize('hooks', "Hooks"), icon: hookIcon }, - [AICustomizationManagementSection.McpServers]: { label: localize('mcpServers', "MCP Servers"), icon: Codicon.server }, - [AICustomizationManagementSection.Plugins]: { label: localize('plugins', "Plugins"), icon: pluginIcon }, - [AICustomizationManagementSection.Models]: { label: localize('models', "Models"), icon: Codicon.vm }, + const sectionInfo: Record = { + [AICustomizationManagementSection.Agents]: { label: localize('agents', "Agents"), icon: agentIcon, description: localize('agentsDesc', "Define custom agents with specialized personas, tool access, and instructions for specific tasks.") }, + [AICustomizationManagementSection.Skills]: { label: localize('skills', "Skills"), icon: skillIcon, description: localize('skillsDesc', "Create reusable skill files that provide domain-specific knowledge and workflows.") }, + [AICustomizationManagementSection.Instructions]: { label: localize('instructions', "Instructions"), icon: instructionsIcon, description: localize('instructionsDesc', "Set always-on instructions that guide AI behavior across your workspace or user profile.") }, + [AICustomizationManagementSection.Prompts]: { label: localize('prompts', "Prompts"), icon: promptIcon, description: localize('promptsDesc', "Reusable prompt templates that can be invoked as slash commands.") }, + [AICustomizationManagementSection.Hooks]: { label: localize('hooks', "Hooks"), icon: hookIcon, description: localize('hooksDesc', "Configure automated actions triggered by events like saving files or running tasks.") }, + [AICustomizationManagementSection.McpServers]: { label: localize('mcpServers', "MCP Servers"), icon: Codicon.server, description: localize('mcpServersDesc', "Connect external tool servers that extend AI capabilities with custom tools and data sources.") }, + [AICustomizationManagementSection.Plugins]: { label: localize('plugins', "Plugins"), icon: pluginIcon, description: localize('pluginsDesc', "Install and manage agent plugins that add additional tools, skills, and integrations.") }, + [AICustomizationManagementSection.Models]: { label: localize('models', "Models"), icon: Codicon.vm, description: localize('modelsDesc', "Configure and manage language models available for use.") }, }; for (const id of this.workspaceService.managementSections) { const info = sectionInfo[id]; @@ -432,9 +432,7 @@ export class AICustomizationManagementEditor extends EditorPane { layout: (width, _, height) => { this.sidebarContainer.style.width = `${width}px`; if (height !== undefined) { - const footerHeight = this.folderPickerContainer?.offsetHeight ?? 0; - const listHeight = height - 8 - footerHeight; - this.sectionsList.layout(listHeight, width); + this.sectionsList.layout(height - 8, width); } }, }, savedWidth, undefined, true); @@ -544,7 +542,7 @@ export class AICustomizationManagementEditor extends EditorPane { 'AICustomizationManagementSections', sectionsListContainer, new SectionItemDelegate(), - [new SectionItemRenderer()], + [new SectionItemRenderer(this.hoverService)], { multipleSelectionSupport: false, setRowLineHeight: false, @@ -617,10 +615,6 @@ export class AICustomizationManagementEditor extends EditorPane { } })); - // Folder picker (sessions window only) - if (this.workspaceService.isSessionsWindow) { - this.createFolderPicker(sidebarContent); - } } private createSidebarHeader(sidebarContent: HTMLElement): void { @@ -629,6 +623,7 @@ export class AICustomizationManagementEditor extends EditorPane { // Home/overview button const homeButton = this.homeButton = DOM.append(headerRow, $('button.sidebar-home-button')); homeButton.setAttribute('aria-label', localize('homeButton', "Overview")); + this.editorDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), homeButton, localize('homeButtonTooltip', "Back to overview"))); const homeIcon = DOM.append(homeButton, $('span.sidebar-home-icon')); homeIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.home)); homeIcon.setAttribute('aria-hidden', 'true'); @@ -741,67 +736,6 @@ export class AICustomizationManagementEditor extends EditorPane { }); } - private createFolderPicker(sidebarContent: HTMLElement): void { - const footer = this.folderPickerContainer = DOM.append(sidebarContent, $('.sidebar-folder-picker')); - - const button = DOM.append(footer, $('button.folder-picker-button')); - button.setAttribute('aria-label', localize('browseFolder', "Browse folder")); - - const folderIcon = DOM.append(button, $(`.codicon.codicon-${Codicon.folder.id}`)); - folderIcon.classList.add('folder-picker-icon'); - - this.folderPickerLabel = DOM.append(button, $('span.folder-picker-label')); - - this.folderPickerClearButton = DOM.append(footer, $('button.folder-picker-clear')); - this.folderPickerClearButton.setAttribute('aria-label', localize('clearFolderOverride', "Reset to session folder")); - DOM.append(this.folderPickerClearButton, $(`.codicon.codicon-${Codicon.close.id}`)); - - // Clicking the main button opens the folder dialog - this.editorDisposables.add(DOM.addDisposableListener(button, 'click', () => { - this.browseForFolder(); - })); - - // Clear button resets to session default - this.editorDisposables.add(DOM.addDisposableListener(this.folderPickerClearButton, 'click', () => { - this.workspaceService.clearOverrideProjectRoot(); - })); - - // Hover showing full path - this.editorDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), button, () => { - const root = this.workspaceService.getActiveProjectRoot(); - return root?.fsPath ?? ''; - })); - - // Keep label and clear button in sync with the active root - this.editorDisposables.add(autorun(reader => { - const root = this.workspaceService.activeProjectRoot.read(reader); - const hasOverride = this.workspaceService.hasOverrideProjectRoot.read(reader); - this.updateFolderPickerLabel(root, hasOverride); - })); - } - - private updateFolderPickerLabel(root: URI | undefined, hasOverride: boolean): void { - if (this.folderPickerLabel) { - this.folderPickerLabel.textContent = root ? basename(root) : localize('noFolder', "No folder"); - } - if (this.folderPickerClearButton) { - this.folderPickerClearButton.style.display = hasOverride ? '' : 'none'; - } - } - - private async browseForFolder(): Promise { - const result = await this.fileDialogService.showOpenDialog({ - canSelectFolders: true, - canSelectFiles: false, - canSelectMany: false, - title: localize('selectFolder', "Select Folder to Explore"), - defaultUri: this.workspaceService.getActiveProjectRoot(), - }); - if (result?.[0]) { - this.workspaceService.setOverrideProjectRoot(result[0]); - } - } - private createWelcomePage(parent: HTMLElement): void { this.welcomePage = this.editorDisposables.add(new AICustomizationWelcomePage( parent, @@ -814,33 +748,35 @@ export class AICustomizationManagementEditor extends EditorPane { this.group.closeEditor(this.input); } }, - prefillChat: (query, options) => { - if (this.workspaceService.isSessionsWindow) { - const widget = this.chatWidgetService.lastFocusedWidget; - if (widget) { - this.chatWidgetService.reveal(widget).then(() => { - widget.setInput(query); - widget.focusInput(); - }); - } else { + prefillChat: async (query, options) => { + try { + if (this.workspaceService.isSessionsWindow) { const sessionsViewId = 'workbench.view.sessions.chat'; - this.viewsService.openView(sessionsViewId, true).then(view => { - const chatView = view as unknown as { prefillInput?(text: string): void; sendQuery?(text: string): void } | undefined; - if (options?.isPartialQuery && chatView?.prefillInput) { - chatView.prefillInput(query); - } else if (chatView?.sendQuery) { - chatView.sendQuery(query); - } - }); + if (options?.newChat) { + await this.commandService.executeCommand('workbench.action.sessions.newChat'); + } + const view = await this.viewsService.openView(sessionsViewId, true); + const chatView = view as unknown as { prefillInput?(text: string): void; sendQuery?(text: string): void } | undefined; + if (options?.isPartialQuery && chatView?.prefillInput) { + chatView.prefillInput(query); + } else if (chatView?.sendQuery) { + chatView.sendQuery(query); + } + } else { + if (options?.newChat) { + await this.commandService.executeCommand('workbench.action.chat.newChat'); + } + await this.commandService.executeCommand('workbench.action.chat.open', { query, isPartialQuery: options?.isPartialQuery ?? false }); } - } else { - this.commandService.executeCommand('workbench.action.chat.open', { query, isPartialQuery: options?.isPartialQuery ?? false }); + } catch (err) { + onUnexpectedError(err); } }, }, this.commandService, this.workspaceService, this.configurationService, + this.hoverService, )); this.welcomePage.rebuildCards(new Set(this.sections.map(s => s.id))); } @@ -1050,6 +986,7 @@ export class AICustomizationManagementEditor extends EditorPane { // Clear persisted section so welcome shows next time this.storageService.remove(AI_CUSTOMIZATION_MANAGEMENT_SELECTED_SECTION_KEY, StorageScope.PROFILE); + this.welcomePage?.reset(); this.updateContentVisibility(); this.ensureSectionsListReflectsActiveSection(undefined); } @@ -1529,6 +1466,7 @@ export class AICustomizationManagementEditor extends EditorPane { this.editorActionButton = DOM.append(editorHeader, $('button.editor-back-button')); this.editorActionButton.setAttribute('aria-label', localize('backToList', "Back to list")); + this.editorDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.editorActionButton, localize('backToListTooltip', "Back to list"))); this.editorActionButtonIcon = DOM.append(this.editorActionButton, $(`.codicon.codicon-${Codicon.arrowLeft.id}.editor-action-button-icon`)); this.editorActionButtonIcon.setAttribute('aria-hidden', 'true'); this.editorDisposables.add(DOM.addDisposableListener(this.editorActionButton, 'click', () => { @@ -1962,6 +1900,7 @@ export class AICustomizationManagementEditor extends EditorPane { const detailHeader = DOM.append(this.mcpDetailContainer, $('.editor-header')); const backButton = DOM.append(detailHeader, $('button.editor-back-button')); backButton.setAttribute('aria-label', localize('backToMcpList', "Back to MCP servers")); + this.editorDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), backButton, localize('backToMcpListTooltip', "Back to MCP servers"))); const backIconEl = DOM.append(backButton, $(`.codicon.codicon-${Codicon.arrowLeft.id}`)); backIconEl.setAttribute('aria-hidden', 'true'); this.editorDisposables.add(DOM.addDisposableListener(backButton, 'click', () => { @@ -2025,6 +1964,7 @@ export class AICustomizationManagementEditor extends EditorPane { const detailHeader = DOM.append(this.pluginDetailContainer, $('.editor-header')); const backButton = DOM.append(detailHeader, $('button.editor-back-button')); backButton.setAttribute('aria-label', localize('backToPluginList', "Back to plugins")); + this.editorDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), backButton, localize('backToPluginListTooltip', "Back to plugins"))); const backIconEl = DOM.append(backButton, $(`.codicon.codicon-${Codicon.arrowLeft.id}`)); backIconEl.setAttribute('aria-hidden', 'true'); this.editorDisposables.add(DOM.addDisposableListener(backButton, 'click', () => { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePage.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePage.ts index f899e74404820..5fcbc7cc20b22 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePage.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePage.ts @@ -11,6 +11,7 @@ import { AICustomizationManagementSection, AI_CUSTOMIZATION_WELCOME_PAGE_VARIANT import { IAICustomizationWorkspaceService, IWelcomePageFeatures } from '../../common/aiCustomizationWorkspaceService.js'; import { ClassicAICustomizationWelcomePage } from './aiCustomizationWelcomePageClassic.js'; import { PromptLaunchersAICustomizationWelcomePage } from './aiCustomizationWelcomePagePromptLaunchers.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; const $ = DOM.$; @@ -21,14 +22,19 @@ export interface IWelcomePageCallbacks { /** * Prefill the chat input with a query. In the sessions window this * uses the sessions chat widget; in core VS Code it opens the chat view. + * + * @param options.newChat When true, always opens a new chat instead of + * reusing the active one. */ - prefillChat(query: string, options?: { isPartialQuery?: boolean }): void; + prefillChat(query: string, options?: { isPartialQuery?: boolean; newChat?: boolean }): void; } export interface IAICustomizationWelcomePageImplementation extends IDisposable { readonly container: HTMLElement; rebuildCards(visibleSectionIds: ReadonlySet): void; focus(): void; + /** Called when the welcome page becomes visible after navigation — clears any transient state. */ + reset?(): void; } /** @@ -48,6 +54,7 @@ export class AICustomizationWelcomePage extends Disposable { private readonly commandService: ICommandService, private readonly workspaceService: IAICustomizationWorkspaceService, private readonly configurationService: IConfigurationService, + private readonly hoverService: IHoverService, ) { super(); @@ -72,6 +79,10 @@ export class AICustomizationWelcomePage extends Disposable { this.implementation.value?.focus(); } + reset(): void { + this.implementation.value?.reset?.(); + } + private renderImplementation(): void { DOM.clearNode(this.container); this.implementation.value = this.createImplementation(); @@ -81,7 +92,7 @@ export class AICustomizationWelcomePage extends Disposable { private createImplementation(): IAICustomizationWelcomePageImplementation { switch (this.getVariant()) { case 'promptLaunchers': - return new PromptLaunchersAICustomizationWelcomePage(this.container, this.welcomePageFeatures, this.callbacks, this.commandService, this.workspaceService); + return new PromptLaunchersAICustomizationWelcomePage(this.container, this.welcomePageFeatures, this.callbacks, this.commandService, this.workspaceService, this.hoverService); case 'classic': default: return new ClassicAICustomizationWelcomePage(this.container, this.welcomePageFeatures, this.callbacks, this.commandService, this.workspaceService); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePageClassic.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePageClassic.ts index 95ac4bd0be771..1c37166a03fe6 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePageClassic.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePageClassic.ts @@ -110,7 +110,7 @@ export class ClassicAICustomizationWelcomePage extends Disposable implements IAI this._register(DOM.addDisposableListener(gettingStarted, 'click', () => { this.callbacks.closeEditor(); if (this.workspaceService.isSessionsWindow) { - this.callbacks.prefillChat('Generate agent customizations. ', { isPartialQuery: true }); + this.callbacks.prefillChat('Generate agent customizations. ', { isPartialQuery: true, newChat: true }); } else { this.commandService.executeCommand('workbench.action.chat.open', { query: '/init ', isPartialQuery: true }); } @@ -169,7 +169,7 @@ export class ClassicAICustomizationWelcomePage extends Disposable implements IAI this.callbacks.closeEditor(); if (this.workspaceService.isSessionsWindow) { const typeLabel = category.label.toLowerCase().replace(/s$/, ''); - this.callbacks.prefillChat(`Create me a custom ${typeLabel} that `, { isPartialQuery: true }); + this.callbacks.prefillChat(`Create me a custom ${typeLabel} that `, { isPartialQuery: true, newChat: true }); } else { this.workspaceService.generateCustomization(category.promptType!); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts index 69a31e7d84831..685f9e226f786 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts @@ -15,6 +15,8 @@ import { agentIcon, instructionsIcon, pluginIcon, skillIcon, hookIcon } from './ import { IAICustomizationWorkspaceService, IWelcomePageFeatures } from '../../common/aiCustomizationWorkspaceService.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import type { IAICustomizationWelcomePageImplementation, IWelcomePageCallbacks } from './aiCustomizationWelcomePage.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; const $ = DOM.$; @@ -34,6 +36,10 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem private cardsContainer: HTMLElement | undefined; private inputElement: HTMLInputElement | undefined; + private sentLabel: HTMLElement | undefined; + private submitBtn: HTMLElement | undefined; + private inputRow: HTMLElement | undefined; + private readonly categoryDescriptions: IPromptLaunchersCategoryDescription[] = [ { id: AICustomizationManagementSection.Agents, @@ -83,6 +89,7 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem private readonly callbacks: IWelcomePageCallbacks, _commandService: ICommandService, private readonly workspaceService: IAICustomizationWorkspaceService, + private readonly hoverService: IHoverService, ) { super(); @@ -107,39 +114,89 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem description.textContent = localize('gettingStartedDesc', "Describe your preferences and conventions to draft agents, skills, and instructions."); const inputRow = DOM.append(gettingStarted, $('.welcome-prompts-input-row')); + this.inputRow = inputRow; this.inputElement = DOM.append(inputRow, $('input.welcome-prompts-input')) as HTMLInputElement; this.inputElement.type = 'text'; this.inputElement.placeholder = localize('workflowInputPlaceholder', "Prefer concise commits, thorough reviews, and tested code..."); this.inputElement.setAttribute('aria-label', localize('workflowInputAriaLabel', "Describe your preferences to customize your agent")); const submitBtn = DOM.append(inputRow, $('button.welcome-prompts-input-submit')); + this.submitBtn = submitBtn; submitBtn.setAttribute('aria-label', localize('workflowSubmitAriaLabel', "Customize agent")); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), submitBtn, localize('workflowSubmitTooltip', "Open in Chat"))); const chevron = DOM.append(submitBtn, $('span.codicon.codicon-arrow-up')); chevron.setAttribute('aria-hidden', 'true'); + const updateSubmitState = () => { + const hasValue = !!(this.inputElement?.value?.trim()); + (submitBtn as HTMLButtonElement).disabled = !hasValue; + submitBtn.classList.toggle('welcome-prompts-input-submit-disabled', !hasValue); + }; + const submit = () => { const value = this.inputElement?.value?.trim(); - this.callbacks.closeEditor(); + if (!value) { + return; + } let query: string; if (this.workspaceService.isSessionsWindow) { - query = value ? `Generate agent customizations. ${value}` : 'Generate agent customizations. '; + query = `Generate agent customizations. ${value}`; } else { - query = value ? `/init ${value}` : '/init '; + query = `/init ${value}`; } - this.callbacks.prefillChat(query, { isPartialQuery: !value }); + + // Show confirmation immediately — before prefillChat so it's visible + // even if prefillChat navigates focus away from this editor + if (this.inputElement) { + this.inputElement.value = ''; + } + updateSubmitState(); + inputRow.classList.add('sent'); + submitBtn.style.display = 'none'; + if (this.sentLabel) { + this.sentLabel.remove(); + } + this.sentLabel = DOM.append(inputRow, $('span.welcome-prompts-sent-label')); + this.sentLabel.textContent = localize('sentToChat', "Sent to chat \u2713"); + + this.callbacks.prefillChat(query, { isPartialQuery: false, newChat: true }); }; - this._register(DOM.addDisposableListener(submitBtn, 'click', submit)); + + this._register(DOM.addDisposableListener(submitBtn, 'click', e => { e.stopPropagation(); submit(); })); this._register(DOM.addDisposableListener(this.inputElement, 'keydown', (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } })); + this._register(DOM.addDisposableListener(this.inputElement, 'input', () => { + updateSubmitState(); + // Typing restores the input row from sent state + this._clearSentState(); + })); + updateSubmitState(); } this.cardsContainer = DOM.append(welcomeInner, $('.welcome-prompts-cards')); } + private _clearSentState(): void { + if (this.sentLabel) { + this.sentLabel.remove(); + this.sentLabel = undefined; + } + if (this.submitBtn) { + this.submitBtn.style.display = ''; + } + if (this.inputRow) { + this.inputRow.classList.remove('sent'); + } + } + + reset(): void { + this._clearSentState(); + } + rebuildCards(visibleSectionIds: ReadonlySet): void { if (!this.cardsContainer) { return; @@ -175,7 +232,7 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem this.callbacks.closeEditor(); if (this.workspaceService.isSessionsWindow) { const typeLabel = category.label.toLowerCase().replace(/s$/, ''); - this.callbacks.prefillChat(`Create me a custom ${typeLabel} that `, { isPartialQuery: true }); + this.callbacks.prefillChat(`Create me a custom ${typeLabel} that `, { isPartialQuery: true, newChat: true }); } else { this.workspaceService.generateCustomization(category.promptType!); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index 32e99b01d0b1c..4517c4e41451c 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -77,66 +77,6 @@ overflow: hidden; } -/* Folder picker footer (sessions window only) */ -.ai-customization-management-editor .sidebar-folder-picker { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 2px; - padding: 6px 4px; - border-top: 1px solid var(--vscode-sideBarSectionHeader-border, transparent); -} - -.ai-customization-management-editor .folder-picker-button { - display: flex; - align-items: center; - gap: 6px; - flex: 1; - min-width: 0; - padding: 4px 6px; - border: none; - border-radius: 4px; - background: transparent; - color: var(--vscode-descriptionForeground); - cursor: pointer; - font-size: 13px; -} - -.ai-customization-management-editor .folder-picker-button:hover { - background-color: var(--vscode-list-hoverBackground); -} - -.ai-customization-management-editor .folder-picker-icon { - flex-shrink: 0; - font-size: 14px; -} - -.ai-customization-management-editor .folder-picker-label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.ai-customization-management-editor .folder-picker-clear { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - border: none; - border-radius: 4px; - background: transparent; - color: var(--vscode-descriptionForeground); - cursor: pointer; - font-size: 12px; -} - -.ai-customization-management-editor .folder-picker-clear:hover { - background-color: var(--vscode-list-hoverBackground); -} - /* Section list items */ .ai-customization-management-editor .section-list-item { display: flex; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css index a1b4e449debc6..4f917fbcdc93c 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css @@ -129,6 +129,33 @@ outline-offset: -1px; } +.ai-customization-management-editor .welcome-prompts-input-submit.welcome-prompts-input-submit-disabled { + opacity: 0.4; + cursor: default; + pointer-events: none; +} + +.ai-customization-management-editor .welcome-prompts-input-row.sent { + justify-content: center; +} + +.ai-customization-management-editor .welcome-prompts-input-row.sent .welcome-prompts-input { + display: none; +} + +.ai-customization-management-editor .welcome-prompts-sent-label { + font-size: 12px; + color: var(--vscode-descriptionForeground); + padding: 0 8px; + white-space: nowrap; + animation: welcomePromptsSentFadeIn 0.2s ease; +} + +@keyframes welcomePromptsSentFadeIn { + from { opacity: 0; transform: translateY(2px); } + to { opacity: 1; transform: translateY(0); } +} + .ai-customization-management-editor .welcome-prompts-input-helper { font-size: 12px; line-height: 1.5; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts index 42ff55300e834..af332edd79309 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts @@ -31,6 +31,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { AuxiliaryBarMaximizedContext } from '../../../common/contextkeys.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { getActiveElement } from '../../../../base/browser/dom.js'; +import { isWeb } from '../../../../base/common/platform.js'; import { IOnboardingService } from '../../welcomeOnboarding/common/onboardingService.js'; import { ONBOARDING_STORAGE_KEY } from '../../welcomeOnboarding/common/onboardingTypes.js'; @@ -235,6 +236,10 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe return; // skip welcome flag is set } + if (isWeb && !this.environmentService.remoteAuthority) { + return; // not supported on web without remote authority (e.g. github.dev) + } + if (!this.configurationService.getValue('workbench.welcomePage.experimentalOnboarding')) { return; // experimental onboarding is disabled } diff --git a/src/vs/workbench/contrib/welcomeOnboarding/browser/onboardingVariationA.ts b/src/vs/workbench/contrib/welcomeOnboarding/browser/onboardingVariationA.ts index c9d06f401f7f5..9c2dcb921ed4a 100644 --- a/src/vs/workbench/contrib/welcomeOnboarding/browser/onboardingVariationA.ts +++ b/src/vs/workbench/contrib/welcomeOnboarding/browser/onboardingVariationA.ts @@ -30,7 +30,9 @@ import { IQuickInputService } from '../../../../platform/quickinput/common/quick import { IFileService } from '../../../../platform/files/common/files.js'; import { IPathService } from '../../../services/path/common/pathService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { InstallChatEvent, InstallChatClassification } from '../../chat/browser/chatSetup/chatSetup.js'; +import { InstallChatEvent, InstallChatClassification, ChatSetupStrategy } from '../../chat/browser/chatSetup/chatSetup.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { OnboardingStepId, ONBOARDING_STEPS, @@ -135,6 +137,8 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi @IFileService private readonly fileService: IFileService, @IPathService private readonly pathService: IPathService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @ICommandService private readonly commandService: ICommandService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { super(); @@ -365,6 +369,8 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi this._renderAgentSessionsSubtitle(this.subtitleEl); } else if (stepId === OnboardingStepId.Personalize) { this._renderPersonalizeSubtitle(this.subtitleEl); + } else if (stepId === OnboardingStepId.Extensions) { + this._renderExtensionsSubtitle(this.subtitleEl); } else { this.subtitleEl.textContent = getOnboardingStepSubtitle(stepId); } @@ -561,6 +567,11 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi if (account) { this._userSignedIn = true; this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); + // Run chat setup in the background (sign-up, extension install, entitlement resolution) + this.commandService.executeCommand('workbench.action.chat.triggerSetup', undefined, { + disableChatViewReveal: true, + setupStrategy: ChatSetupStrategy.DefaultSetup, + }); this._nextStep(); } } catch (error) { @@ -592,6 +603,10 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi if (account) { this._userSignedIn = true; this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); + this.commandService.executeCommand('workbench.action.chat.triggerSetup', undefined, { + disableChatViewReveal: true, + setupStrategy: ChatSetupStrategy.DefaultSetup, + }); this._nextStep(); } } catch (error) { @@ -743,6 +758,7 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi } pill.classList.add('selected'); pill.setAttribute('aria-checked', 'true'); + this.accessibilityService.alert(localize('onboarding.keymap.selected.alert', "{0} keyboard mapping selected", keymap.label)); })); } const selectedKeymapIndex = keymapOptions.findIndex(k => k.id === this.selectedKeymapId); @@ -765,6 +781,20 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi ); } + private _renderExtensionsSubtitle(container: HTMLElement): void { + clearNode(container); + const modifier = isMacintosh ? 'Cmd' : 'Ctrl'; + container.append( + localize('onboarding.extensions.subtitle.prefix', "Install extensions to enhance your workflow. Press "), + this._createKbd(localize({ key: 'onboarding.extensions.subtitle.modifier', comment: ['Keyboard modifier key'] }, "{0}", modifier)), + '+', + this._createKbd(localize('onboarding.extensions.subtitle.shift', "Shift")), + '+', + this._createKbd(localize('onboarding.extensions.subtitle.x', "X")), + localize('onboarding.extensions.subtitle.suffix', " to browse the Extension Marketplace."), + ); + } + private _createThemeCard(parent: HTMLElement, theme: IOnboardingThemeOption, allCards: HTMLElement[]): void { const card = this._registerStepFocusable(append(parent, $('div.onboarding-a-theme-card'))); allCards.push(card); @@ -795,6 +825,7 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi } card.classList.add('selected'); card.setAttribute('aria-checked', 'true'); + this.accessibilityService.alert(localize('onboarding.theme.selected.alert', "{0} theme selected", theme.label)); })); this.stepDisposables.add(addDisposableListener(card, EventType.KEY_DOWN, (e: KeyboardEvent) => { @@ -841,6 +872,7 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi const installBtn = this._registerStepFocusable(append(row, $('button.onboarding-a-ext-install'))); installBtn.type = 'button'; installBtn.textContent = localize('onboarding.ext.install', "Install"); + installBtn.setAttribute('aria-label', localize('onboarding.ext.install.aria', "Install {0}", ext.name)); this.stepDisposables.add(addDisposableListener(installBtn, EventType.CLICK, () => { this._logAction('installExtension', undefined, ext.id); @@ -850,6 +882,8 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi () => { installBtn.textContent = localize('onboarding.ext.installed', "Installed"); installBtn.classList.add('installed'); + installBtn.setAttribute('aria-label', localize('onboarding.ext.installed.aria', "{0} installed", ext.name)); + this.accessibilityService.alert(localize('onboarding.ext.installed.alert', "{0} has been installed", ext.name)); }, () => { installBtn.textContent = localize('onboarding.ext.install', "Install"); @@ -1068,6 +1102,7 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi c.setAttribute('aria-checked', c.dataset.id === option.id ? 'true' : 'false'); } this._applyAiPreference(option.id); + this.accessibilityService.alert(localize('onboarding.aiPref.selected.alert', "{0} selected", option.label)); })); } const selectedAiIndex = ONBOARDING_AI_PREFERENCE_OPTIONS.findIndex(o => o.id === this.selectedAiMode); @@ -1148,7 +1183,10 @@ export class OnboardingVariationA extends Disposable implements IOnboardingServi } private _createFeatureCard(parent: HTMLElement, icon: ThemeIcon, title: string, description?: string): HTMLElement { - const card = append(parent, $('div.onboarding-a-feature-card')); + const card = this._registerStepFocusable(append(parent, $('div.onboarding-a-feature-card'))); + card.setAttribute('tabindex', '0'); + card.setAttribute('role', 'group'); + card.setAttribute('aria-label', title); const iconCol = append(card, $('div.onboarding-a-feature-icon')); iconCol.appendChild(renderIcon(icon)); const textCol = append(card, $('div.onboarding-a-feature-text')); diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts index 6d9f30a89e80e..d79ff8d6bd526 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts @@ -14,7 +14,7 @@ import { constObservable, observableValue } from '../../../../../base/common/obs import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { IDialogService, IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IListService, ListService } from '../../../../../platform/list/browser/listService.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; @@ -499,7 +499,6 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor reg.defineInstance(IWorkingCopyService, new class extends mock() { override readonly onDidChangeDirty = Event.None; }()); - reg.defineInstance(IFileDialogService, new class extends mock() { }()); reg.defineInstance(IExtensionService, new class extends mock() { }()); reg.defineInstance(IQuickInputService, new class extends mock() { }()); reg.defineInstance(IViewsService, new class extends mock() { diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationWelcomePages.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationWelcomePages.fixture.ts index ae229cc4c76ea..47ed3b442e240 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationWelcomePages.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationWelcomePages.fixture.ts @@ -18,6 +18,7 @@ import { AICustomizationWelcomePage } from '../../../../contrib/chat/browser/aiC import { ClassicAICustomizationWelcomePage } from '../../../../contrib/chat/browser/aiCustomization/aiCustomizationWelcomePageClassic.js'; import { PromptLaunchersAICustomizationWelcomePage } from '../../../../contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.js'; import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; +import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js'; import '../../../../../platform/theme/common/colors/inputColors.js'; import '../../../../../platform/theme/common/colors/listColors.js'; @@ -115,6 +116,7 @@ function renderPromptLaunchersWelcomePage(ctx: ComponentFixtureContext): void { }, createMockCommandService(), workspaceService, + NullHoverService, )); page.rebuildCards(visibleSections); } @@ -137,6 +139,7 @@ function renderSelectedWelcomePage(ctx: ComponentFixtureContext, variant: AICust createMockCommandService(), workspaceService, configService, + NullHoverService, )); page.rebuildCards(visibleSections); }