From 957e119f8a10984b727dc0a8855d0a99db1d55d7 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 24 Apr 2026 10:27:56 +0200 Subject: [PATCH 01/33] actions: lazy-update MenuWorkbenchToolBar based on visibility --- src/vs/platform/actions/browser/toolbar.ts | 45 ++++++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index e44cdb4eae07e..3e6c4c8816e64 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -12,7 +12,7 @@ import { intersection } from '../../../base/common/collections.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { Emitter } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; -import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { DisposableStore, toDisposable, IDisposable } from '../../../base/common/lifecycle.js'; import { localize } from '../../../nls.js'; import { createActionViewItem, getActionBarActions } from './menuEntryActionViewItem.js'; import { IMenu, IMenuActionOptions, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from '../common/actions.js'; @@ -285,6 +285,29 @@ export class WorkbenchToolBar extends ToolBar { // ---- MenuWorkbenchToolBar ------------------------------------------------- +let sharedIntersectionObserver: IntersectionObserver | undefined; +const intersectionObserverCallbacks = new WeakMap void>(); + +function observeVisibility(element: Element, callback: (isVisible: boolean) => void): IDisposable { + if (!sharedIntersectionObserver) { + sharedIntersectionObserver = new IntersectionObserver((entries) => { + for (const entry of entries) { + const cb = intersectionObserverCallbacks.get(entry.target); + if (cb) { + cb(entry.isIntersecting); + } + } + }); + } + + intersectionObserverCallbacks.set(element, callback); + sharedIntersectionObserver.observe(element); + + return toDisposable(() => { + intersectionObserverCallbacks.delete(element); + sharedIntersectionObserver?.unobserve(element); + }); +} export interface IToolBarRenderOptions { /** @@ -337,6 +360,7 @@ export class MenuWorkbenchToolBar extends WorkbenchToolBar { private readonly _menuOptions: IMenuActionOptions | undefined; private readonly _toolbarOptions: IToolBarRenderOptions | undefined; private readonly _container: HTMLElement; + private readonly _viewDisposables = this._store.add(new DisposableStore()); constructor( container: HTMLElement, @@ -374,13 +398,18 @@ export class MenuWorkbenchToolBar extends WorkbenchToolBar { // update logic this._menu = this._store.add(menuService.createMenu(menuId, contextKeyService, { emitEventsForSubmenuChanges: true, eventDebounceDelay: options?.eventDebounceDelay })); - this._store.add(this._menu.onDidChange(() => { - this._updateToolbar(); - this._onDidChangeMenuItems.fire(this); - })); - - this._store.add(actionViewService.onDidChange(e => { - if (e === menuId) { + this._store.add(observeVisibility(this._container, isVisible => { + this._viewDisposables.clear(); + if (isVisible) { + this._viewDisposables.add(this._menu.onDidChange(() => { + this._updateToolbar(); + this._onDidChangeMenuItems.fire(this); + })); + this._viewDisposables.add(actionViewService.onDidChange(e => { + if (e === menuId) { + this._updateToolbar(); + } + })); this._updateToolbar(); } })); From f32c7986c2715a9533bc69492b369b62de450816 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 24 Apr 2026 11:17:19 +0200 Subject: [PATCH 02/33] address review: per-window observer + fallback --- src/vs/platform/actions/browser/toolbar.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index 3e6c4c8816e64..c6cd829925737 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -12,7 +12,7 @@ import { intersection } from '../../../base/common/collections.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { Emitter } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; -import { DisposableStore, toDisposable, IDisposable } from '../../../base/common/lifecycle.js'; +import { DisposableStore, toDisposable, IDisposable, Disposable } from '../../../base/common/lifecycle.js'; import { localize } from '../../../nls.js'; import { createActionViewItem, getActionBarActions } from './menuEntryActionViewItem.js'; import { IMenu, IMenuActionOptions, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from '../common/actions.js'; @@ -285,12 +285,20 @@ export class WorkbenchToolBar extends ToolBar { // ---- MenuWorkbenchToolBar ------------------------------------------------- -let sharedIntersectionObserver: IntersectionObserver | undefined; +const sharedIntersectionObservers = new WeakMap(); const intersectionObserverCallbacks = new WeakMap void>(); function observeVisibility(element: Element, callback: (isVisible: boolean) => void): IDisposable { - if (!sharedIntersectionObserver) { - sharedIntersectionObserver = new IntersectionObserver((entries) => { + const targetWindow = getWindow(element); + if (typeof targetWindow.IntersectionObserver !== 'function') { + // fallback: assume always visible + callback(true); + return Disposable.None; + } + + let observer = sharedIntersectionObservers.get(targetWindow); + if (!observer) { + observer = new targetWindow.IntersectionObserver((entries) => { for (const entry of entries) { const cb = intersectionObserverCallbacks.get(entry.target); if (cb) { @@ -298,14 +306,15 @@ function observeVisibility(element: Element, callback: (isVisible: boolean) => v } } }); + sharedIntersectionObservers.set(targetWindow, observer); } intersectionObserverCallbacks.set(element, callback); - sharedIntersectionObserver.observe(element); + observer.observe(element); return toDisposable(() => { intersectionObserverCallbacks.delete(element); - sharedIntersectionObserver?.unobserve(element); + observer.unobserve(element); }); } From 5d1f84570273a79aade6bb3679acc76f823ffb37 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 24 Apr 2026 11:31:03 +0200 Subject: [PATCH 03/33] chat: fix editor change event checking by using .matches() --- .../chat/browser/chatEditing/chatEditingServiceImpl.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index ba31aec8a0f89..aeb355759dfc0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -237,10 +237,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const enum K { Stream, Workspace } const editsSeen: ({ kind: K.Stream; seen: number; stream: IStreamingEdits } | { kind: K.Workspace })[] = []; - let editorDidChange = false; - const editorListener = Event.once(this._editorService.onDidActiveEditorChange)(() => { - editorDidChange = true; - }); + const initialActiveEditor = this._editorService.activeEditorPane?.input; const editorOpenPromises = new ResourceMap>(); const openChatEditedFiles = this._configurationService.getValue('accessibility.openChatEditedFiles'); @@ -252,6 +249,8 @@ export class ChatEditingService extends Disposable implements IChatEditingServic editorOpenPromises.set(uri, (async () => { if (this.notebookService.getNotebookTextModel(uri) || uri.scheme === Schemas.untitled || await this._fileService.exists(uri).catch(() => false)) { const activeUri = this._editorService.activeEditorPane?.input.resource; + const currentActiveEditor = this._editorService.activeEditorPane?.input; + const editorDidChange = initialActiveEditor && currentActiveEditor ? !initialActiveEditor.matches(currentActiveEditor) : initialActiveEditor !== currentActiveEditor; const inactive = editorDidChange || this._editorService.activeEditorPane?.input instanceof ChatEditorInput && isEqual(this._editorService.activeEditorPane.input.sessionResource, session.chatSessionResource) || Boolean(activeUri && session.entries.get().find(entry => isEqual(activeUri, entry.modifiedURI))); @@ -270,7 +269,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic editsSeen.length = 0; editorOpenPromises.clear(); - editorListener.dispose(); }; const handleResponseParts = async () => { From 9ffb703bd6a1cf67b4a049806e07fd3552a3946b Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 24 Apr 2026 10:33:23 +0100 Subject: [PATCH 04/33] feat: enhance chat input animation for improved accessibility and responsiveness Co-authored-by: Copilot --- src/vs/sessions/browser/media/style.css | 16 +++++-- .../browser/widget/input/chatInputPart.ts | 47 +++++++++++++++++++ .../chat/browser/widget/media/chat.css | 25 ++++++++-- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index 228ae5d448f30..f69d321937f99 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -429,11 +429,19 @@ border-color: var(--vscode-agentsChatInput-focusBorder, var(--vscode-focusBorder)) !important; } -/* While the developer-joy animated border is active, suppress the static - border so it doesn't visually conflict with the spinning gradient ring. */ -.agent-sessions-workbench .interactive-session .chat-input-container.working, +/* While the developer-joy animated border is active, keep a faint + persistent ring so the input still has a visible boundary throughout + the comet animation (improves visuals + accessibility). The spinning + gradient ring overlays this. Focused state continues to use the + regular focus border defined above. */ +.agent-sessions-workbench .interactive-session .chat-input-container.working:not(.focused) { + border-color: var(--vscode-agentsChatInput-border) !important; +} + +/* Dim the focus border while the comet is animating so the bright focus + ring doesn't visually fight with the spinning gradient. */ .agent-sessions-workbench .interactive-session .chat-input-container.working.focused { - border-color: transparent !important; + border-color: color-mix(in srgb, var(--vscode-agentsChatInput-focusBorder, var(--vscode-focusBorder)) 40%, transparent) !important; } /* Make the Monaco editor inside the chat input transparent so it inherits the chatInput.background */ 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 a38fa48c0a2d3..73a6e2cdd7ada 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -3506,10 +3506,57 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge layout(width: number) { this.cachedWidth = width; this._stableInputPartWidth.set(width, undefined); + this._updateWorkingProgressAnimationDuration(width); return this._layout(width); } + /** + * Scale the working/progress border comet animation duration with + * the input width so the comet's perceived linear travel speed (the + * rate it sweeps along the perimeter in px/sec) stays roughly + * constant. A fixed cycle time made wide inputs feel sluggish, but + * an aggressive inverse curve made narrow inputs feel slow because + * their cycle was clamped while the comet had little distance to + * cover. Linear-with-width + tight clamps keeps both extremes + * looking lively. + */ + private _lastAnimDurationS: number | undefined; + private _updateWorkingProgressAnimationDuration(width: number): void { + if (!this.inputContainer) { + return; + } + // Sub-linear scaling: cycle time grows with width but tapers off + // so wide inputs still feel snappy. Tuned so ~400px → ~1.7s and + // ~1000px → ~2.3s rather than ~4s. + const MIN_DURATION_S = 1.4; + const MAX_DURATION_S = 2.5; + const safeWidth = Math.max(50, width); + const raw = 0.55 + 0.075 * Math.sqrt(safeWidth); + const duration = Math.min(MAX_DURATION_S, Math.max(MIN_DURATION_S, raw)); + + // Skip no-op updates (e.g. repeated layout calls during steady state). + if (this._lastAnimDurationS !== undefined && Math.abs(this._lastAnimDurationS - duration) < 0.05) { + return; + } + this._lastAnimDurationS = duration; + this.inputContainer.style.setProperty('--chat-input-anim-duration', `${duration.toFixed(2)}s`); + + // CSS animations capture animation-duration at start time and most + // browsers do not re-pick up values that come from a custom + // property mid-flight. If the comet is currently spinning, force + // a restart so the new cadence takes effect immediately. Toggling + // the .working class would cancel the in-flight indicator state, + // so instead we briefly remove and re-add the animation by + // flipping a marker class that the CSS uses to swap animation-name. + if (this.inputContainer.classList.contains('working')) { + this.inputContainer.classList.add('chat-input-anim-restart'); + // Force a style recalc so the next frame restarts the animation. + void this.inputContainer.offsetWidth; + this.inputContainer.classList.remove('chat-input-anim-restart'); + } + } + private get _effectiveInputEditorMaxHeight(): number { if (this._maxHeight === undefined) { return this.inputEditorMaxHeight; 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 7cf400fcbd506..9864d359d5390 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -862,6 +862,11 @@ have to be updated for changes to the rules above, or to support more deeply nes width: 100%; position: relative; transition: box-shadow 350ms ease; + /* Duration of the working/progress border comet animation. Set + dynamically by `ChatInputPart#layout` to keep the comet's linear + travel speed roughly constant regardless of input width — wider + inputs would otherwise feel sluggish at a fixed duration. */ + --chat-input-anim-duration: 4s; } /* Prevent contents from covering border corners. Not applied in compact mode @@ -967,18 +972,32 @@ have to be updated for changes to the rules above, or to support more deeply nes } .monaco-workbench .interactive-session .chat-input-container.working { - border-color: transparent; + /* Keep a faint, persistent ring around the input while the comet + animates around it. This preserves a visible boundary for the + input throughout the animation (including the dim portion of the + perimeter behind the comet) and improves contrast for users who + rely on a visible focus/container outline for accessibility. */ + border-color: var(--vscode-input-border, transparent); overflow: visible; } .monaco-workbench .interactive-session .chat-input-container.working::before { opacity: 1; - animation: chat-input-working-border-spin 4s linear infinite; + animation: chat-input-working-border-spin var(--chat-input-anim-duration) linear infinite; } .monaco-workbench .interactive-session .chat-input-container.working::after { opacity: 1; - animation: chat-input-working-border-spin 4s linear infinite; + animation: chat-input-working-border-spin var(--chat-input-anim-duration) linear infinite; +} + +/* Marker class toggled briefly by `ChatInputPart#_updateWorkingProgressAnimationDuration` + to force a restart of the comet animations so a new + `--chat-input-anim-duration` takes effect mid-flight (browsers cache + animation-duration at start time when sourced from a custom property). */ +.monaco-workbench .interactive-session .chat-input-container.working.chat-input-anim-restart::before, +.monaco-workbench .interactive-session .chat-input-container.working.chat-input-anim-restart::after { + animation: none; } @media (prefers-reduced-motion: reduce) { From c916396feace15c350415b44f19351aa2a5d1ac5 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 24 Apr 2026 10:33:36 +0100 Subject: [PATCH 05/33] feat: add chat input animation duration variable to stylelint configuration --- build/lib/stylelint/vscode-known-variables.json | 1 + 1 file changed, 1 insertion(+) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index a46bef0bbab71..ce2c2f708dd47 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1041,6 +1041,7 @@ "--animation-angle", "--animation-opacity", "--chat-input-anim-angle", + "--chat-input-anim-duration", "--chat-send-button-anim-angle", "--chat-setup-dialog-glow-angle", "--vscode-chat-font-family", From cfce5175243db91d35375a1987274f8e8231d2a7 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 24 Apr 2026 10:47:39 +0100 Subject: [PATCH 06/33] Update src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/widget/input/chatInputPart.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 73a6e2cdd7ada..80a9317cbbf1f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -3544,16 +3544,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // CSS animations capture animation-duration at start time and most // browsers do not re-pick up values that come from a custom - // property mid-flight. If the comet is currently spinning, force - // a restart so the new cadence takes effect immediately. Toggling - // the .working class would cancel the in-flight indicator state, - // so instead we briefly remove and re-add the animation by - // flipping a marker class that the CSS uses to swap animation-name. + // property mid-flight. If the comet is currently spinning, restart + // it on the next animation frame so style and layout changes can + // batch without forcing a synchronous reflow. Toggling the .working + // class would cancel the in-flight indicator state, so instead we + // briefly flip a marker class that the CSS uses to swap + // animation-name. if (this.inputContainer.classList.contains('working')) { - this.inputContainer.classList.add('chat-input-anim-restart'); - // Force a style recalc so the next frame restarts the animation. - void this.inputContainer.offsetWidth; - this.inputContainer.classList.remove('chat-input-anim-restart'); + const inputContainer = this.inputContainer; + inputContainer.classList.add('chat-input-anim-restart'); + dom.scheduleAtNextAnimationFrame(dom.getWindow(inputContainer), () => { + inputContainer.classList.remove('chat-input-anim-restart'); + }); } } From 99482aaf152785ebb0d9e2628cc195a62a7305fb Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 24 Apr 2026 10:47:49 +0100 Subject: [PATCH 07/33] Update src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 80a9317cbbf1f..68153748c4027 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -3518,8 +3518,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge * constant. A fixed cycle time made wide inputs feel sluggish, but * an aggressive inverse curve made narrow inputs feel slow because * their cycle was clamped while the comet had little distance to - * cover. Linear-with-width + tight clamps keeps both extremes - * looking lively. + * cover. Sub-linear scaling with width (`sqrt(width)`) plus tight + * clamps keeps both extremes looking lively. */ private _lastAnimDurationS: number | undefined; private _updateWorkingProgressAnimationDuration(width: number): void { From 7022c4a0d3e1d8d48bf7e75701cd685362d02bcd Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 24 Apr 2026 10:48:00 +0100 Subject: [PATCH 08/33] Update src/vs/sessions/browser/media/style.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/sessions/browser/media/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index f69d321937f99..b800b9c8015ca 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -432,8 +432,8 @@ /* While the developer-joy animated border is active, keep a faint persistent ring so the input still has a visible boundary throughout the comet animation (improves visuals + accessibility). The spinning - gradient ring overlays this. Focused state continues to use the - regular focus border defined above. */ + gradient ring overlays this. When focused, the regular focus border + is blended/dimmed during the animation to avoid competing visually. */ .agent-sessions-workbench .interactive-session .chat-input-container.working:not(.focused) { border-color: var(--vscode-agentsChatInput-border) !important; } From 750552da031f6f55f13651c85dff4a2a4a174659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20Kapano=C4=9Flu?= Date: Fri, 24 Apr 2026 05:34:18 -0500 Subject: [PATCH 09/33] add Turkish DOS (CP 857) encoding support (#300114) * add Turkish DOS (CP 857) encoding support * merge in --------- Co-authored-by: Benjamin Pasero --- .../services/textfile/common/encoding.ts | 35 +++++++++++-------- src/vscode-dts/vscode.d.ts | 2 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/services/textfile/common/encoding.ts b/src/vs/workbench/services/textfile/common/encoding.ts index b119273a6ffa7..4e401ed6f98a2 100644 --- a/src/vs/workbench/services/textfile/common/encoding.ts +++ b/src/vs/workbench/services/textfile/common/encoding.ts @@ -693,85 +693,90 @@ export const SUPPORTED_ENCODINGS: EncodingsMap = { labelShort: 'ISO 8859-9', order: 33 }, + cp857: { + labelLong: 'Turkish (CP 857)', + labelShort: 'CP 857', + order: 34 + }, windows1258: { labelLong: 'Vietnamese (Windows 1258)', labelShort: 'Windows 1258', - order: 34 + order: 35 }, gbk: { labelLong: 'Simplified Chinese (GBK)', labelShort: 'GBK', - order: 35 + order: 36 }, gb18030: { labelLong: 'Simplified Chinese (GB18030)', labelShort: 'GB18030', - order: 36 + order: 37 }, cp950: { labelLong: 'Traditional Chinese (Big5)', labelShort: 'Big5', - order: 37, + order: 38, guessableName: 'Big5' }, big5hkscs: { labelLong: 'Traditional Chinese (Big5-HKSCS)', labelShort: 'Big5-HKSCS', - order: 38 + order: 39 }, shiftjis: { labelLong: 'Japanese (Shift JIS)', labelShort: 'Shift JIS', - order: 39, + order: 40, guessableName: 'SHIFT_JIS' }, eucjp: { labelLong: 'Japanese (EUC-JP)', labelShort: 'EUC-JP', - order: 40, + order: 41, guessableName: 'EUC-JP' }, euckr: { labelLong: 'Korean (EUC-KR)', labelShort: 'EUC-KR', - order: 41, + order: 42, guessableName: 'EUC-KR' }, windows874: { labelLong: 'Thai (Windows 874)', labelShort: 'Windows 874', - order: 42 + order: 43 }, iso885911: { labelLong: 'Latin/Thai (ISO 8859-11)', labelShort: 'ISO 8859-11', - order: 43 + order: 44 }, koi8ru: { labelLong: 'Cyrillic (KOI8-RU)', labelShort: 'KOI8-RU', - order: 44 + order: 45 }, koi8t: { labelLong: 'Tajik (KOI8-T)', labelShort: 'KOI8-T', - order: 45 + order: 46 }, gb2312: { labelLong: 'Simplified Chinese (GB 2312)', labelShort: 'GB 2312', - order: 46, + order: 47, guessableName: 'GB2312' }, cp865: { labelLong: 'Nordic DOS (CP 865)', labelShort: 'CP 865', - order: 47 + order: 48 }, cp850: { labelLong: 'Western European DOS (CP 850)', labelShort: 'CP 850', - order: 48 + order: 49 } }; diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 329826a2894cc..df78e006e8e0f 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -130,7 +130,7 @@ declare module 'vscode' { * 'iso88597', 'windows1255', 'iso88598', 'iso885910', 'iso885916', 'windows1254', * 'iso88599', 'windows1258', 'gbk', 'gb18030', 'cp950', 'big5hkscs', 'shiftjis', * 'eucjp', 'euckr', 'windows874', 'iso885911', 'koi8ru', 'koi8t', 'gb2312', - * 'cp865', 'cp850'. + * 'cp865', 'cp850', 'cp857'. */ readonly encoding: string; From c020167edc1500f8748be8f100fbe744f14e86cb Mon Sep 17 00:00:00 2001 From: xingsy97 <87063252+xingsy97@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:56:21 +0800 Subject: [PATCH 10/33] contextkey: fix scanner returning '>=' instead of '>' for greater-than operator (#307059) --- src/vs/platform/contextkey/common/scanner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/contextkey/common/scanner.ts b/src/vs/platform/contextkey/common/scanner.ts index 64dff3d76fcf3..68aa701350699 100644 --- a/src/vs/platform/contextkey/common/scanner.ts +++ b/src/vs/platform/contextkey/common/scanner.ts @@ -133,7 +133,7 @@ export class Scanner { case TokenType.LtEq: return '<='; case TokenType.Gt: - return '>='; + return '>'; case TokenType.GtEq: return '>='; case TokenType.RegexOp: From 274f8c8c2e793bc80c0d9a5dc6887658119d1bff Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:35:56 +0000 Subject: [PATCH 11/33] MultiFileDiffEditor - fix flickering on diff editors when switching resources (#312328) --- .../multiDiffEditorViewModel.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 22c2a2e13ff1c..7ef1bfef61f4b 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -72,23 +72,30 @@ export class MultiDiffEditorViewModel extends Disposable { const allItems = mapObservableArrayCached( this, this._documentsArr, - (d, store) => store.add(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this)) + (d, store) => store.add(RefCounted.create(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this))) ).recomputeInitiallyAndOnChange(this._store); - const waitForNewDiffs: IObservable> = derived(this, reader => { + const waitForNewDiffs: IObservable[]>> = derived(this, reader => { const next = allItems.read(reader); - const unresolved = next.filter(i => !i.waitForInitialDiffOr1s.promiseResult.read(undefined)); + const unresolved = next.filter(i => !i.object.waitForInitialDiffOr1s.promiseResult.read(undefined)); if (unresolved.length === 0) { return ObservablePromise.resolved(next); } return new ObservablePromise( - Promise.all(unresolved.map(i => i.waitForInitialDiffOr1s.promise)).then(() => next) + Promise.all(unresolved.map(i => i.object.waitForInitialDiffOr1s.promise)).then(() => next) ); }); - const resolved = new ObservableResolvedPromise(waitForNewDiffs, [] as readonly DocumentDiffItemViewModel[], this._store); + const resolved = new ObservableResolvedPromise(waitForNewDiffs, [] as readonly RefCounted[], this._store); + + this.items = derived(this, reader => { + const resolvedItems = resolved.lastResolved.read(reader); + return resolvedItems.map(i => { + const ref = reader.store.add(i.createNewRef(i)); + return ref.object; + }); + }); - this.items = resolved.lastResolved; this.isLoading = derived(this, reader => this._documents.read(reader) === 'loading' || resolved.isResolving.read(reader) ); From b091f1db1ebdd7bb3f15232465e4d65f0bd47957 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 24 Apr 2026 14:15:03 +0200 Subject: [PATCH 12/33] refactor: clean up CODEOWNERS and CODENOTIFY by removing unnecessary entries (#312327) --- .github/CODENOTIFY | 12 ------------ .github/CODEOWNERS | 4 ++-- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index 60bd1fbce0ea3..805ff8fa9da6b 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -1,6 +1,5 @@ # Base Utilities src/vs/base/common/oauth.ts @TylerLeonhardt -src/vs/base/common/uri.ts @jrieken src/vs/base/browser/domSanitize.ts @mjbvz src/vs/base/parts/quickinput/** @TylerLeonhardt @@ -18,13 +17,6 @@ src/vs/platform/quickinput/** @TylerLeonhardt src/vs/platform/secrets/** @TylerLeonhardt src/vs/platform/terminal/electron-main/** @anthonykim1 src/vs/platform/terminal/node/** @anthonykim1 -src/vs/platform/actions/common/menuService.ts @jrieken -src/vs/platform/instantiation/** @jrieken - -# Editor Core -src/vs/editor/contrib/snippet/** @jrieken -src/vs/editor/contrib/suggest/** @jrieken -src/vs/editor/contrib/format/** @jrieken # Electron Main src/vs/code/** @deepak1556 @@ -74,9 +66,5 @@ extensions/git/** @lszomoru extensions/git-base/** @lszomoru extensions/github/** @lszomoru -# Chat Editing, Inline Chat -src/vs/workbench/contrib/chat/browser/chatEditing/** @jrieken -src/vs/workbench/contrib/inlineChat/** @jrieken - # Testing test/sanity/** @dmitrivMS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9d23fc89077a0..68e1f902d414e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,5 +16,5 @@ build/lib/policies/policyData.jsonc @joshspicer @rebornix @joaomoreno @pwang347 # VS Code API # Ensure the API team is aware of changes to the vscode-dts file # this is only about the final API, not about proposed API changes -src/vscode-dts/vscode.d.ts @jrieken @mjbvz @alexr00 -src/vs/workbench/services/extensions/common/extensionPoints.json @jrieken @mjbvz @alexr00 +src/vscode-dts/vscode.d.ts @mjbvz @alexr00 +src/vs/workbench/services/extensions/common/extensionPoints.json @mjbvz @alexr00 From cbbe8995fe4dfe067b8e3c3470604f59ed21ee21 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 24 Apr 2026 14:15:21 +0200 Subject: [PATCH 13/33] fixes https://github.com/microsoft/vscode/issues/303069 (#312332) Co-authored-by: Copilot --- .../chat/browser/chatEditing/chatEditingEditorOverlay.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index f1064adb378ad..35ddb2477e172 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -29,6 +29,7 @@ import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels. import { ThemeIcon } from '../../../../../base/common/themables.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; +import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; export class ChatEditingAcceptRejectActionViewItem extends ActionViewItem { @@ -319,6 +320,11 @@ class ChatEditingOverlayController { activeEditorSignal.read(r); // signal const editor = group.activeEditorPane; + + if (!getCodeEditor(editor?.getControl())) { + return undefined; + } + const uri = EditorResourceAccessor.getOriginalUri(editor?.input, { supportSideBySide: SideBySideEditor.PRIMARY }); return uri; From 75dcfb925986c9d19df3b00f7b9a7c0a62b41f77 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 24 Apr 2026 14:15:41 +0200 Subject: [PATCH 14/33] refactor: replace IInlineChatEditStrategy with InlineChatToolCalling for improved edit handling (#312329) * refactor: update import paths for InlineChat2Prompt components * refactor: remove unused inline chat configuration and telemetry data Co-authored-by: Copilot * refactor: replace IInlineChatEditStrategy with InlineChatToolCalling for improved edit handling --------- Co-authored-by: Copilot --- extensions/copilot/package.json | 10 -- extensions/copilot/package.nls.json | 1 - .../inlineChat/node/inlineChatIntent.ts | 151 +----------------- .../node/inlineChatPrompt.tsx} | 24 +-- .../extension/intents/node/editCodeIntent.ts | 1 - .../prompt/node/chatParticipantTelemetry.ts | 12 -- .../node/inline/inlineChatEditCodePrompt.tsx | 16 +- .../inline/test/inlineChat2Prompt.spec.tsx | 2 +- .../common/configurationService.ts | 1 - 9 files changed, 22 insertions(+), 196 deletions(-) rename extensions/copilot/src/extension/{prompts/node/inline/inlineChat2Prompt.tsx => inlineChat/node/inlineChatPrompt.tsx} (93%) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index e9fd55fa0d20f..6a7d00d955ac0 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -4268,16 +4268,6 @@ "experimental" ] }, - "github.copilot.chat.inlineChat.selectionRatioThreshold": { - "type": "number", - "default": 0, - "markdownDescription": "%github.copilot.config.inlineChat.selectionRatioThreshold%", - "tags": [ - "advanced", - "experimental", - "onExp" - ] - }, "github.copilot.chat.inlineChat.reasoningEffort": { "type": "string", "default": "low", diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index 5151887763a28..26cd798a0a55b 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -375,7 +375,6 @@ "github.copilot.config.notebook.alternativeNESFormat.enabled": "Enable alternative format for Next Edit Suggestions in notebooks.", "github.copilot.config.localWorkspaceRecording.enabled": "Enable local workspace recording for analysis.", "github.copilot.config.editRecording.enabled": "Enable edit recording for analysis.", - "github.copilot.config.inlineChat.selectionRatioThreshold": "Threshold at which to switch editing strategies for inline chat. When a selection portion of code matches a parse tree node, only that is presented to the language model. This speeds up response times but might have lower quality results. Requires having a parse tree for the document and the `inlineChat.enableV2` setting. Values must be between 0 and 1, where 0 means off and 1 means the selection perfectly matches a parse tree node.", "github.copilot.config.inlineChat.reasoningEffort": "Controls the reasoning effort level for inline chat requests. Lower values result in faster responses with fewer reasoning tokens. Supported values depend on the model.", "github.copilot.config.inlineChat.enableThinking": "Controls whether thinking/reasoning is enabled for inline chat requests. When disabled, reasoning summaries are suppressed for faster responses.", "github.copilot.config.debug.requestLogger.maxEntries": "Maximum number of entries to keep in the request logger for debugging purposes.", diff --git a/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts b/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts index 0ac62436d769e..04efff275d1d5 100644 --- a/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts +++ b/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts @@ -8,7 +8,6 @@ import { Raw } from '@vscode/prompt-tsx'; import { BudgetExceededError } from '@vscode/prompt-tsx/dist/base/materialized'; import type * as vscode from 'vscode'; import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; -import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher'; import { CanceledResult, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IEditSurvivalTrackerService } from '../../../platform/editSurvivalTracking/common/editSurvivalTrackerService'; @@ -16,18 +15,14 @@ import { IEndpointProvider } from '../../../platform/endpoint/common/endpointPro import { IOctoKitService } from '../../../platform/github/common/githubService'; import { IIgnoreService } from '../../../platform/ignore/common/ignoreService'; import { ILogService } from '../../../platform/log/common/logService'; -import { Prediction } from '../../../platform/networking/common/fetch'; import { IChatEndpoint, IMakeChatRequestOptions } from '../../../platform/networking/common/networking'; -import { IParserService } from '../../../platform/parser/node/parserService'; -import { getWasmLanguage } from '../../../platform/parser/node/treeSitterLanguages'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl'; import { toErrorMessage } from '../../../util/common/errorMessage'; import { isNonEmptyArray } from '../../../util/vs/base/common/arrays'; -import { AsyncIterableSource, timeout } from '../../../util/vs/base/common/async'; +import { timeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { ResourceSet } from '../../../util/vs/base/common/map'; -import { clamp } from '../../../util/vs/base/common/numbers'; import { isFalsyOrWhitespace } from '../../../util/vs/base/common/strings'; import { assertType, isDefined } from '../../../util/vs/base/common/types'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; @@ -35,7 +30,6 @@ import { ChatRequestEditorData, ChatResponseTextEditPart, LanguageModelTextPart, import { Intent } from '../../common/constants'; import { getAgentTools } from '../../intents/node/agentIntent'; import { IIntentService } from '../../intents/node/intentService'; -import { SelectionSplitKind, SummarizedDocumentData, SummarizedDocumentSplitMetadata } from '../../intents/node/testIntent/summarizedDocumentWithSelection'; import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection'; import { Conversation, Turn } from '../../prompt/common/conversation'; import { IToolCall } from '../../prompt/common/intents'; @@ -43,16 +37,14 @@ import { ToolCallRound } from '../../prompt/common/toolCallRound'; import { ChatTelemetryBuilder, InlineChatTelemetry } from '../../prompt/node/chatParticipantTelemetry'; import { DefaultIntentRequestHandler } from '../../prompt/node/defaultIntentRequestHandler'; import { IDocumentContext } from '../../prompt/node/documentContext'; -import { IIntent, NoopReplyInterpreter, ReplyInterpreterMetaData, TelemetryData } from '../../prompt/node/intents'; -import { ResponseProcessorContext } from '../../prompt/node/responseProcessorContext'; +import { IIntent } from '../../prompt/node/intents'; import { PromptRenderer } from '../../prompts/node/base/promptRenderer'; -import { ICompletedToolCallRound, InlineChat2Prompt, LARGE_FILE_LINE_THRESHOLD } from '../../prompts/node/inline/inlineChat2Prompt'; -import { InlineChatEditCodePrompt } from '../../prompts/node/inline/inlineChatEditCodePrompt'; +import { ICompletedToolCallRound, InlineChat2Prompt, LARGE_FILE_LINE_THRESHOLD } from './inlineChatPrompt'; import { ToolName } from '../../tools/common/toolNames'; import { CopilotToolMode } from '../../tools/common/toolsRegistry'; import { isToolValidationError, isValidatedToolInput, IToolsService } from '../../tools/common/toolsService'; import { InlineChatProgressMessages } from './progressMessages'; -import { CopilotInteractiveEditorResponse, InteractionOutcome, InteractionOutcomeComputer } from './promptCraftingTypes'; +import { CopilotInteractiveEditorResponse, InteractionOutcome } from './promptCraftingTypes'; const INLINE_CHAT_EXIT_TOOL_NAME = 'inline_chat_exit'; @@ -64,9 +56,6 @@ interface IInlineChatEditResult { errorMessage?: string; } -interface IInlineChatEditStrategy { - executeEdit(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise; -} export class InlineChatIntent implements IIntent { @@ -97,8 +86,6 @@ export class InlineChatIntent implements IIntent { @IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService, @IIntentService private readonly _intentService: IIntentService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IParserService private readonly _parserService: IParserService, - @IExperimentationService private readonly _experimentationService: IExperimentationService, @IOctoKitService private readonly _octoKitService: IOctoKitService, ) { this._progressMessages = this._instantiationService.createInstance(InlineChatProgressMessages); @@ -201,22 +188,6 @@ export class InlineChatIntent implements IIntent { } }); - // Don't use edit tools when the selection seems good enough - let useToolsForEdit = true; - const selectionRatioThreshold = clamp(this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatSelectionRatioThreshold, this._experimentationService), 0, 1); - if (!documentContext.selection.isEmpty - && selectionRatioThreshold > 0 - && getWasmLanguage(documentContext.document.languageId) - ) { - const data = await SummarizedDocumentData.create(this._parserService, documentContext.document, documentContext.fileIndentInfo, documentContext.selection, SelectionSplitKind.Adjusted); - const { adjusted, original } = data.offsetSelections; - const ratio = original.length / adjusted.length; - if (ratio <= 1 && ratio >= selectionRatioThreshold) { - request = { ...request, command: Intent.Edit }; - useToolsForEdit = false; - } - } - // Start generating contextual message immediately const contextualMessagePromise = this._progressMessages.getContextualMessage(request.prompt, documentContext, token); @@ -228,11 +199,9 @@ export class InlineChatIntent implements IIntent { let result: IInlineChatEditResult; try { - const strategy: IInlineChatEditStrategy = useToolsForEdit - ? this._instantiationService.createInstance(InlineChatEditToolsStrategy, this) - : this._instantiationService.createInstance(InlineChatEditHeuristicStrategy, this); + const inlineToolLoop = this._instantiationService.createInstance(InlineChatToolCalling, this); - result = await strategy.executeEdit(endpoint, conversation, request, stream, token, documentContext, chatTelemetry); + result = await inlineToolLoop.run(endpoint, conversation, request, stream, token, documentContext, chatTelemetry); } catch (err) { this._logService.error(err, 'InlineChatIntent: prompt rendering failed'); return { @@ -294,11 +263,7 @@ export class InlineChatIntent implements IIntent { } } -class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { - - readonly id = InlineChatIntent.ID; - readonly locations = [ChatLocation.Editor]; - readonly description = ''; +class InlineChatToolCalling { constructor( private readonly _intent: InlineChatIntent, @@ -309,7 +274,7 @@ class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { @IExperimentationService private readonly _experimentationService: IExperimentationService, ) { } - async executeEdit(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise { + async run(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise { assertType(request.location2 instanceof ChatRequestEditorData); assertType(documentContext); @@ -596,103 +561,3 @@ class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { return result; } } - -class InlineChatEditHeuristicStrategy implements IInlineChatEditStrategy { - - readonly id = InlineChatIntent.ID; - readonly locations = [ChatLocation.Editor]; - readonly description = ''; - - constructor( - private readonly _intent: InlineChatIntent, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IExperimentationService private readonly _experimentationService: IExperimentationService, - ) { } - - async executeEdit(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise { - - assertType(request.location2 instanceof ChatRequestEditorData); - - const outcomeComputer = new InteractionOutcomeComputer(request.location2.document.uri); - const renderer = PromptRenderer.create(this._instantiationService, endpoint, InlineChatEditCodePrompt, { - ignoreCustomInstructions: true, - documentContext, - promptContext: { - query: request.prompt, - chatVariables: new ChatVariablesCollection([...request.references]), - history: conversation.turns.slice(0, -1), - } - }); - - const renderResult = await renderer.render(undefined, token, { trace: true }); - - const replyInterpreter = renderResult.metadata.get(ReplyInterpreterMetaData)?.replyInterpreter ?? new NoopReplyInterpreter(); - const telemetryData = renderResult.metadata.getAll(TelemetryData); - - const telemetry = chatTelemetry.makeRequest(this._intent, ChatLocation.Editor, conversation, renderResult.messages, renderResult.tokenCount, renderResult.references, endpoint, telemetryData, 0, 0); - - stream = ChatResponseStreamImpl.spy(stream, part => { - if (part instanceof ChatResponseTextEditPart) { - telemetry.markEmittedEdits(part.uri, part.edits); - } - }); - - let prediction: Prediction | undefined; - const documentSplit = renderResult.metadata.get(SummarizedDocumentSplitMetadata)?.split; - if (documentSplit) { - prediction = { - type: 'content', - content: '' - }; - prediction.content = `\`\`\`${documentContext.document.languageId}\n${documentSplit.codeSelected}\n\`\`\``; - } - - const source = new AsyncIterableSource(); - const responseProcessing = replyInterpreter.processResponse(new ResponseProcessorContext(conversation.sessionId, conversation.getLatestTurn(), renderResult.messages, outcomeComputer), source.asyncIterable, stream, token); - - const fetchResult = await endpoint.makeChatRequest2({ - debugName: 'InlineChat2Intent', - messages: renderResult.messages, - userInitiatedRequest: true, - location: ChatLocation.Editor, - modelCapabilities: { - enableThinking: this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatEnableThinking, this._experimentationService), - reasoningEffort: typeof request.modelConfiguration?.reasoningEffort === 'string' - ? request.modelConfiguration.reasoningEffort - : this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatReasoningEffort, this._experimentationService), - }, - telemetryProperties: { - messageId: telemetry.telemetryMessageId, - conversationId: telemetry.sessionId, - messageSource: this._intent.id - }, - requestOptions: { - stream: true, - prediction - }, - finishedCb: async (_text, _index, delta) => { - telemetry.markReceivedToken(); - source.emitOne({ delta }); - return undefined; - } - }, token); - - source.resolve(); - - await responseProcessing; - - const responseText = fetchResult.type === ChatFetchResponseType.Success ? fetchResult.value : ''; - telemetry.sendTelemetry( - fetchResult.requestId, fetchResult.type, responseText, - new InteractionOutcome(telemetry.editCount > 0 ? 'inlineEdit' : 'none', []), - [] - ); - - return { - needsExitTool: telemetry.editCount === 0 && fetchResult.type === ChatFetchResponseType.Success, - lastResponse: fetchResult, - telemetry, - }; - } -} diff --git a/extensions/copilot/src/extension/prompts/node/inline/inlineChat2Prompt.tsx b/extensions/copilot/src/extension/inlineChat/node/inlineChatPrompt.tsx similarity index 93% rename from extensions/copilot/src/extension/prompts/node/inline/inlineChat2Prompt.tsx rename to extensions/copilot/src/extension/inlineChat/node/inlineChatPrompt.tsx index 2fc270c057615..c06ea2c82b740 100644 --- a/extensions/copilot/src/extension/prompts/node/inline/inlineChat2Prompt.tsx +++ b/extensions/copilot/src/extension/inlineChat/node/inlineChatPrompt.tsx @@ -6,18 +6,18 @@ import { AssistantMessage, PromptElement, PromptElementProps, PromptReference, PromptSizing, SystemMessage, ToolMessage, useKeepWith, UserMessage } from '@vscode/prompt-tsx'; import { ChatResponsePart } from '@vscode/prompt-tsx/dist/base/vscodeTypes'; import type { CancellationToken, ExtendedLanguageModelToolResult, Position, Progress } from 'vscode'; -import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot'; -import { CacheType } from '../../../../platform/endpoint/common/endpointTypes'; -import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService'; -import { ChatRequest, ChatRequestEditorData, Range } from '../../../../vscodeTypes'; -import { ChatVariablesCollection } from '../../../prompt/common/chatVariablesCollection'; -import { IToolCall } from '../../../prompt/common/intents'; -import { CopilotIdentityRules } from '../base/copilotIdentity'; -import { SafetyRules } from '../base/safetyRules'; -import { Tag } from '../base/tag'; -import { ChatVariables, UserQuery } from '../panel/chatVariables'; -import { CodeBlock } from '../panel/safeElements'; -import { ToolResult } from '../panel/toolCalling'; +import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot'; +import { CacheType } from '../../../platform/endpoint/common/endpointTypes'; +import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; +import { ChatRequest, ChatRequestEditorData, Range } from '../../../vscodeTypes'; +import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection'; +import { IToolCall } from '../../prompt/common/intents'; +import { CopilotIdentityRules } from '../../prompts/node/base/copilotIdentity'; +import { SafetyRules } from '../../prompts/node/base/safetyRules'; +import { Tag } from '../../prompts/node/base/tag'; +import { ChatVariables, UserQuery } from '../../prompts/node/panel/chatVariables'; +import { CodeBlock } from '../../prompts/node/panel/safeElements'; +import { ToolResult } from '../../prompts/node/panel/toolCalling'; /** diff --git a/extensions/copilot/src/extension/intents/node/editCodeIntent.ts b/extensions/copilot/src/extension/intents/node/editCodeIntent.ts index 011fee98596c0..4e8cb4c0ba845 100644 --- a/extensions/copilot/src/extension/intents/node/editCodeIntent.ts +++ b/extensions/copilot/src/extension/intents/node/editCodeIntent.ts @@ -421,7 +421,6 @@ export class EditCodeIntentInvocation implements IIntentInvocation { // Don't report file references that came in via chat variables in an editing session, unless they have warnings, // because they are already displayed as part of the working set references: result.references.filter((ref) => this.shouldKeepReference(editCodeStep, ref, toolReferences, chatVariables)), - // telemetryData: result.metadata.getAll(DocumentToAstSelectionData) }; } diff --git a/extensions/copilot/src/extension/prompt/node/chatParticipantTelemetry.ts b/extensions/copilot/src/extension/prompt/node/chatParticipantTelemetry.ts index da9a241e61247..ece8b8d188047 100644 --- a/extensions/copilot/src/extension/prompt/node/chatParticipantTelemetry.ts +++ b/extensions/copilot/src/extension/prompt/node/chatParticipantTelemetry.ts @@ -25,7 +25,6 @@ import { DiagnosticsTelemetryData, findDiagnosticsTelemetry } from '../../inline import { InteractionOutcome } from '../../inlineChat/node/promptCraftingTypes'; import { AgentIntent } from '../../intents/node/agentIntent'; import { EditCodeIntent } from '../../intents/node/editCodeIntent'; -import { DocumentToAstSelectionData } from '../../prompts/node/inline/inlineChatEditCodePrompt'; import { getCustomInstructionTelemetry } from '../../prompts/node/panel/customInstructions'; import { PATCH_PREFIX } from '../../tools/node/applyPatch/parseApplyPatch'; import { ChatVariablesCollection, parseSlashCommand } from '../common/chatVariablesCollection'; @@ -205,8 +204,6 @@ type RequestInlineTelemetryMeasurements = RequestTelemetryMeasurements & { selectionProblemsCount: number; diagnosticsCount: number; selectionDiagnosticsCount: number; - userSelectionLength: number; - adjustedSelectionLength: number; }; //#endregion @@ -903,11 +900,6 @@ export class InlineChatTelemetry extends ChatTelemetry { return acc; }, {} as Record); - - const selectionData = this._getTelemetryData(DocumentToAstSelectionData); - const userSelectionLength = selectionData?.original.length ?? -1; - const adjustedSelectionLength = selectionData?.adjusted.length ?? -1; - /* __GDPR__ "inline.request" : { "owner": "digitarald", @@ -954,8 +946,6 @@ export class InlineChatTelemetry extends ChatTelemetry { "numToolCalls": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The total number of tool calls" }, "availableToolCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How number of tools that were available." }, "toolTokenCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many tokens were used by tool definitions." }, - "userSelectionLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The length of the user selection" }, - "adjustedSelectionLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The length of the adjusted user selection" }, "isBYOK": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the request was for a BYOK model" }, "isAuto": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the request was for an Auto model" } } @@ -1001,8 +991,6 @@ export class InlineChatTelemetry extends ChatTelemetry { numToolCalls: toolCalls.length, availableToolCount: this._availableToolCount, toolTokenCount: this._toolTokenCount, - userSelectionLength, - adjustedSelectionLength, isBYOK: isBYOKModel(this._endpoint), isAuto: isAutoModel(this._endpoint) } satisfies RequestInlineTelemetryMeasurements); diff --git a/extensions/copilot/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx b/extensions/copilot/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx index b4eb9762ec08f..3653ed26a503e 100644 --- a/extensions/copilot/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx +++ b/extensions/copilot/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx @@ -11,10 +11,9 @@ import { IParserService } from '../../../../platform/parser/node/parserService'; import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService'; import { isNotebookCellOrNotebookChatInput } from '../../../../util/common/notebooks'; import { illegalArgument } from '../../../../util/vs/base/common/errors'; -import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange'; import { GenericInlinePromptProps } from '../../../context/node/resolvers/genericInlineIntentInvocation'; import { SelectionSplitKind, SummarizedDocumentData, SummarizedDocumentWithSelection } from '../../../intents/node/testIntent/summarizedDocumentWithSelection'; -import { EarlyStopping, LeadingMarkdownStreaming, TelemetryData } from '../../../prompt/node/intents'; +import { EarlyStopping, LeadingMarkdownStreaming } from '../../../prompt/node/intents'; import { TextPieceClassifiers } from '../../../prompt/node/streamingEdits'; import { InstructionMessage } from '../base/instructionMessage'; import { LegacySafetyRules } from '../base/safetyRules'; @@ -26,15 +25,6 @@ import { ProjectLabels } from '../panel/projectLabels'; import { LanguageServerContextPrompt } from './languageServerContextPrompt'; import { SummarizedDocumentSplit } from './promptingSummarizedDocument'; -export class DocumentToAstSelectionData extends TelemetryData { - - constructor( - readonly original: OffsetRange, - readonly adjusted: OffsetRange, - ) { - super(); - } -} export interface InlineChatEditCodePromptProps extends GenericInlinePromptProps { readonly ignoreCustomInstructions?: boolean; @@ -117,10 +107,6 @@ export class InlineChatEditCodePrompt extends PromptElement {data.hasCodeWithoutSelection && <>The modified {data.placeholderText} code with ``` is:} - ); } diff --git a/extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx b/extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx index 77be8ce02991c..08cfac485c668 100644 --- a/extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx +++ b/extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx @@ -8,7 +8,7 @@ import { TextDocumentSnapshot } from '../../../../../platform/editing/common/tex import { createTextDocumentData, setDocText } from '../../../../../util/common/test/shims/textDocument'; import { URI } from '../../../../../util/vs/base/common/uri'; import { ExtendedLanguageModelToolResult, LanguageModelTextPart, LanguageModelToolResult, Position, Range } from '../../../../../vscodeTypes'; -import { FileContextElement, FileSelectionElement, ICompletedToolCallRound, LARGE_FILE_LINE_THRESHOLD, ToolCallRoundsElement } from '../inlineChat2Prompt'; +import { FileContextElement, FileSelectionElement, ICompletedToolCallRound, LARGE_FILE_LINE_THRESHOLD, ToolCallRoundsElement } from '../../../../inlineChat/node/inlineChatPrompt'; function createSnapshot(content: string, languageId: string = 'typescript'): TextDocumentSnapshot { const uri = URI.file('/workspace/file.ts'); diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 93d52257f1ba9..936bb3a2c16f7 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -642,7 +642,6 @@ export namespace ConfigKey { export const NotebookAlternativeDocumentFormat = defineAndMigrateExpSetting('chat.advanced.notebook.alternativeFormat', 'chat.notebook.alternativeFormat', AlternativeNotebookFormat.xml); export const UseAlternativeNESNotebookFormat = defineAndMigrateExpSetting('chat.advanced.notebook.alternativeNESFormat.enabled', 'chat.notebook.alternativeNESFormat.enabled', false); - export const InlineChatSelectionRatioThreshold = defineSetting('chat.inlineChat.selectionRatioThreshold', ConfigType.ExperimentBased, 0); export const InlineChatReasoningEffort = defineSetting('chat.inlineChat.reasoningEffort', ConfigType.ExperimentBased, 'low'); export const InlineChatEnableThinking = defineSetting('chat.inlineChat.enableThinking', ConfigType.ExperimentBased, false); From 4f5dd1c0f62fc37ce3cf705b52f672cc66c60024 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:34:14 +0000 Subject: [PATCH 15/33] Agents - hide "Open File" and "Open Changes" action for folders (#312204) --- .../changes/browser/changesViewActions.ts | 8 +++-- .../changes/browser/changesViewRenderer.ts | 30 +++++++++---------- .../contrib/changes/common/changes.ts | 1 + 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts index 53df074fa2fb4..9e395451c21bb 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts @@ -13,7 +13,7 @@ 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_ID } from '../common/changes.js'; +import { ActiveSessionContextKeys, CHANGES_VIEW_ID, ChangesContextKeys } 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'; @@ -150,12 +150,14 @@ class OpenFileAction extends Action2 { id: MenuId.ChatEditingSessionChangeToolbar, group: 'navigation', order: 1, - when: IsSessionsWindowContext, alt: { id: 'workbench.action.agentSessions.openChanges', title: localize2('openChanges', "Open Changes"), icon: Codicon.gitCompare, - } + }, + when: ContextKeyExpr.and( + IsSessionsWindowContext, + ChangesContextKeys.ChangeKind.isEqualTo('file')) } }); } diff --git a/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts b/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts index cc31d1a26a639..5ace9f05f5a7e 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts @@ -15,7 +15,7 @@ import { IResourceNode, ResourceTree } from '../../../../base/common/resourceTre import { URI } from '../../../../base/common/uri.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { FileKind } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; @@ -24,7 +24,7 @@ import { bindContextKey } from '../../../../platform/observable/common/platformO import { IResourceLabel, ResourceLabels } from '../../../../workbench/browser/labels.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; -import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { ModifiedFileEntryState } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { GITHUB_REMOTE_FILE_SCHEME, ISessionFileChange } from '../../../services/sessions/common/session.js'; import { ActiveSessionContextKeys, ChangesContextKeys, ChangesViewMode } from '../common/changes.js'; @@ -156,7 +156,7 @@ export function buildTreeChildren(items: IChangesFileItem[], treeRootInfo?: ICha interface IChangesTreeTemplate { readonly label: IResourceLabel; readonly toolbar: MenuWorkbenchToolBar | undefined; - readonly contextKeyService: IContextKeyService | undefined; + readonly changeKindContextKey: IContextKey<'root' | 'folder' | 'file'>; readonly reviewCommentsBadge: HTMLElement; readonly agentFeedbackBadge: HTMLElement; readonly decorationBadge: HTMLElement; @@ -218,10 +218,12 @@ export class ChangesTreeRenderer implements ICompressibleTreeRenderer, _index: number, templateData: IChangesTreeTemplate): void { @@ -261,9 +263,8 @@ export class ChangesTreeRenderer implements ICompressibleTreeRenderer, templateData: IChangesTreeTemplate): void { @@ -402,9 +401,8 @@ export class ChangesTreeRenderer implements ICompressibleTreeRenderer, _index: number, templateData: IChangesTreeTemplate): void { diff --git a/src/vs/sessions/contrib/changes/common/changes.ts b/src/vs/sessions/contrib/changes/common/changes.ts index 05f5df42f8365..82d57d7b90d34 100644 --- a/src/vs/sessions/contrib/changes/common/changes.ts +++ b/src/vs/sessions/contrib/changes/common/changes.ts @@ -27,6 +27,7 @@ export const enum IsolationMode { } export const ChangesContextKeys = { + ChangeKind: new RawContextKey<'root' | 'folder' | 'file'>('sessions.changeKind', 'file'), VersionMode: new RawContextKey('sessions.changesVersionMode', ChangesVersionMode.BranchChanges), ViewMode: new RawContextKey('sessions.changesViewMode', ChangesViewMode.List) }; From a41f4f54251c40fbe7bbafcd641abcf18701bfa6 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 24 Apr 2026 15:39:18 +0200 Subject: [PATCH 16/33] chore: inline cleanup (#312341) * chore: move inline chat 2 into its own folder, move other things around * chore: move stuff into better locations Co-authored-by: Copilot --------- Co-authored-by: Copilot --- .../test/vscode-node/inlineChat.test.ts | 66 ------- .../node/inlineChatIntent.ts | 20 +- .../node/inlineChatPrompt.tsx | 0 .../test/node}/inlineChat2Prompt.spec.tsx | 10 +- .../vscode-node/naturalLanguageHint.test.ts | 0 .../vscode-node/inlineCompletionProvider.ts | 2 +- .../vscode-node/naturalLanguageHint.ts | 0 .../src/extension/intents/node/allIntents.ts | 2 +- .../browser/nullInlineChatSessionService.ts | 8 +- .../browser/actions/chatCodeblockActions.ts | 2 +- .../browser/actions/codeBlockOperations.ts | 2 +- .../chat/browser/actions/reviewEdits.ts | 146 ++++++++++++++ .../browser/inlineChat.contribution.ts | 16 +- .../browser/inlineChatController.ts | 183 +----------------- .../browser/inlineChatDefaultModel.ts | 2 - .../inlineChat/browser/inlineChatNotebook.ts | 2 - .../browser/inlineChatSessionService.ts | 38 +--- .../browser/inlineChatSessionServiceImpl.ts | 14 +- .../inlineChat/browser/inlineChatWidget.ts | 152 ++------------- .../contrib/inlineChat/common/inlineChat.ts | 10 +- .../chat/browser/terminalChatWidget.ts | 140 +++++++++++++- 21 files changed, 358 insertions(+), 457 deletions(-) delete mode 100644 extensions/copilot/src/extension/inlineChat/test/vscode-node/inlineChat.test.ts rename extensions/copilot/src/extension/{inlineChat => inlineChat2}/node/inlineChatIntent.ts (98%) rename extensions/copilot/src/extension/{inlineChat => inlineChat2}/node/inlineChatPrompt.tsx (100%) rename extensions/copilot/src/extension/{prompts/node/inline/test => inlineChat2/test/node}/inlineChat2Prompt.spec.tsx (98%) rename extensions/copilot/src/extension/{inlineChat => inlineEdits}/test/vscode-node/naturalLanguageHint.test.ts (100%) rename extensions/copilot/src/extension/{inlineChat => inlineEdits}/vscode-node/naturalLanguageHint.ts (100%) create mode 100644 src/vs/workbench/contrib/chat/browser/actions/reviewEdits.ts diff --git a/extensions/copilot/src/extension/inlineChat/test/vscode-node/inlineChat.test.ts b/extensions/copilot/src/extension/inlineChat/test/vscode-node/inlineChat.test.ts deleted file mode 100644 index 6111a174626c7..0000000000000 --- a/extensions/copilot/src/extension/inlineChat/test/vscode-node/inlineChat.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as vscode from 'vscode'; -import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; - -suite('Inline Chat', function () { - // this.timeout(1000 * 60 * 1); // 1 minute - - let store: DisposableStore; - - teardown(function () { - store.dispose(); - }); - - setup(function () { - store = new DisposableStore(); - }); - - test.skip('E2E Inline Chat Test', async function () { - store.add(vscode.lm.registerLanguageModelChatProvider('test', new class implements vscode.LanguageModelChatProvider { - async provideLanguageModelChatInformation(options: { silent: boolean }, token: vscode.CancellationToken): Promise { - return [{ - id: 'test', - name: 'test', - family: 'test', - version: '0.0.0', - maxInputTokens: 1000, - maxOutputTokens: 1000, - requiresAuthorization: true, - capabilities: {} - }]; - } - async provideLanguageModelChatResponse(model: vscode.LanguageModelChatInformation, messages: Array, options: vscode.ProvideLanguageModelChatResponseOptions, progress: vscode.Progress, token: vscode.CancellationToken): Promise { - throw new Error('Method not implemented.'); - } - async provideTokenCount(model: vscode.LanguageModelChatInformation, text: string | vscode.LanguageModelChatMessage | vscode.LanguageModelChatMessage2, token: vscode.CancellationToken): Promise { - return 0; - } - })); - - - - // Create and open a new file - const document = await vscode.workspace.openTextDocument({ language: 'javascript' }); - await vscode.window.showTextDocument(document); - - try { - - await vscode.commands.executeCommand('vscode.editorChat.start', { - blockOnResponse: true, - autoSend: true, - message: 'Write me a for loop in javascript', - position: new vscode.Position(0, 0), - initialSelection: new vscode.Selection(0, 0, 0, 0), - modelSelector: { id: 'test' } - }); - } catch (err) { - assert.ok(false); - } - - }); -}); diff --git a/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts similarity index 98% rename from extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts rename to extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts index 04efff275d1d5..c5575cf6dfed1 100644 --- a/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts +++ b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts @@ -43,8 +43,8 @@ import { ICompletedToolCallRound, InlineChat2Prompt, LARGE_FILE_LINE_THRESHOLD } import { ToolName } from '../../tools/common/toolNames'; import { CopilotToolMode } from '../../tools/common/toolsRegistry'; import { isToolValidationError, isValidatedToolInput, IToolsService } from '../../tools/common/toolsService'; -import { InlineChatProgressMessages } from './progressMessages'; -import { CopilotInteractiveEditorResponse, InteractionOutcome } from './promptCraftingTypes'; +import { InlineChatProgressMessages } from '../../inlineChat/node/progressMessages'; +import { CopilotInteractiveEditorResponse, InteractionOutcome } from '../../inlineChat/node/promptCraftingTypes'; const INLINE_CHAT_EXIT_TOOL_NAME = 'inline_chat_exit'; @@ -61,12 +61,7 @@ export class InlineChatIntent implements IIntent { static readonly ID = Intent.InlineChat; - static readonly _EDIT_TOOLS = new Set([ - ToolName.ApplyPatch, - ToolName.EditFile, - ToolName.ReplaceString, - ToolName.MultiReplaceString, - ]); + readonly id = InlineChatIntent.ID; @@ -265,6 +260,13 @@ export class InlineChatIntent implements IIntent { class InlineChatToolCalling { + private static readonly _EDIT_TOOLS = new Set([ + ToolName.ApplyPatch, + ToolName.EditFile, + ToolName.ReplaceString, + ToolName.MultiReplaceString, + ]); + constructor( private readonly _intent: InlineChatIntent, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -515,7 +517,7 @@ class InlineChatToolCalling { assertType(request.location2 instanceof ChatRequestEditorData); - const enabledTools = new Set(InlineChatIntent._EDIT_TOOLS); + const enabledTools = new Set(InlineChatToolCalling._EDIT_TOOLS); if (!request.location2.selection.isEmpty) { // only used the multi-replace when there is no selection enabledTools.delete(ToolName.MultiReplaceString); diff --git a/extensions/copilot/src/extension/inlineChat/node/inlineChatPrompt.tsx b/extensions/copilot/src/extension/inlineChat2/node/inlineChatPrompt.tsx similarity index 100% rename from extensions/copilot/src/extension/inlineChat/node/inlineChatPrompt.tsx rename to extensions/copilot/src/extension/inlineChat2/node/inlineChatPrompt.tsx diff --git a/extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx b/extensions/copilot/src/extension/inlineChat2/test/node/inlineChat2Prompt.spec.tsx similarity index 98% rename from extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx rename to extensions/copilot/src/extension/inlineChat2/test/node/inlineChat2Prompt.spec.tsx index 08cfac485c668..24d7db6ebca5d 100644 --- a/extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx +++ b/extensions/copilot/src/extension/inlineChat2/test/node/inlineChat2Prompt.spec.tsx @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { expect, suite, test } from 'vitest'; -import { TextDocumentSnapshot } from '../../../../../platform/editing/common/textDocumentSnapshot'; -import { createTextDocumentData, setDocText } from '../../../../../util/common/test/shims/textDocument'; -import { URI } from '../../../../../util/vs/base/common/uri'; -import { ExtendedLanguageModelToolResult, LanguageModelTextPart, LanguageModelToolResult, Position, Range } from '../../../../../vscodeTypes'; -import { FileContextElement, FileSelectionElement, ICompletedToolCallRound, LARGE_FILE_LINE_THRESHOLD, ToolCallRoundsElement } from '../../../../inlineChat/node/inlineChatPrompt'; +import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot'; +import { createTextDocumentData, setDocText } from '../../../../util/common/test/shims/textDocument'; +import { URI } from '../../../../util/vs/base/common/uri'; +import { ExtendedLanguageModelToolResult, LanguageModelTextPart, LanguageModelToolResult, Position, Range } from '../../../../vscodeTypes'; +import { FileContextElement, FileSelectionElement, ICompletedToolCallRound, LARGE_FILE_LINE_THRESHOLD, ToolCallRoundsElement } from '../../node/inlineChatPrompt'; function createSnapshot(content: string, languageId: string = 'typescript'): TextDocumentSnapshot { const uri = URI.file('/workspace/file.ts'); diff --git a/extensions/copilot/src/extension/inlineChat/test/vscode-node/naturalLanguageHint.test.ts b/extensions/copilot/src/extension/inlineEdits/test/vscode-node/naturalLanguageHint.test.ts similarity index 100% rename from extensions/copilot/src/extension/inlineChat/test/vscode-node/naturalLanguageHint.test.ts rename to extensions/copilot/src/extension/inlineEdits/test/vscode-node/naturalLanguageHint.test.ts diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts index f59c7a000a97d..f19d7d44be50f 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts @@ -35,7 +35,7 @@ import { autorun, IObservable, observableFromEvent } from '../../../util/vs/base import { basename } from '../../../util/vs/base/common/path'; import { StringEdit } from '../../../util/vs/editor/common/core/edits/stringEdit'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; -import { LineCheck } from '../../inlineChat/vscode-node/naturalLanguageHint'; +import { LineCheck } from './naturalLanguageHint'; import { createCorrelationId } from '../common/correlationId'; import { NesChangeHint } from '../common/nesTriggerHint'; import { NESInlineCompletionContext } from '../node/nextEditProvider'; diff --git a/extensions/copilot/src/extension/inlineChat/vscode-node/naturalLanguageHint.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/naturalLanguageHint.ts similarity index 100% rename from extensions/copilot/src/extension/inlineChat/vscode-node/naturalLanguageHint.ts rename to extensions/copilot/src/extension/inlineEdits/vscode-node/naturalLanguageHint.ts diff --git a/extensions/copilot/src/extension/intents/node/allIntents.ts b/extensions/copilot/src/extension/intents/node/allIntents.ts index a21db2ccafdc5..1a14e3a38defb 100644 --- a/extensions/copilot/src/extension/intents/node/allIntents.ts +++ b/extensions/copilot/src/extension/intents/node/allIntents.ts @@ -5,7 +5,7 @@ import { SyncDescriptor } from '../../../util/vs/platform/instantiation/common/descriptors'; -import { InlineChatIntent } from '../../inlineChat/node/inlineChatIntent'; +import { InlineChatIntent } from '../../inlineChat2/node/inlineChatIntent'; import { IntentRegistry } from '../../prompt/node/intentRegistry'; import { AgentIntent } from './agentIntent'; import { AskAgentIntent } from './askAgentIntent'; diff --git a/src/vs/sessions/contrib/chat/browser/nullInlineChatSessionService.ts b/src/vs/sessions/contrib/chat/browser/nullInlineChatSessionService.ts index 857f9033f5085..28c4fd671ed33 100644 --- a/src/vs/sessions/contrib/chat/browser/nullInlineChatSessionService.ts +++ b/src/vs/sessions/contrib/chat/browser/nullInlineChatSessionService.ts @@ -7,7 +7,7 @@ import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IInlineChatSession2, IInlineChatSessionService } from '../../../../workbench/contrib/inlineChat/browser/inlineChatSessionService.js'; +import { IInlineChatSession, IInlineChatSessionService } from '../../../../workbench/contrib/inlineChat/browser/inlineChatSessionService.js'; class NullInlineChatSessionService implements IInlineChatSessionService { declare _serviceBrand: undefined; @@ -17,15 +17,15 @@ class NullInlineChatSessionService implements IInlineChatSessionService { dispose(): void { } - createSession(_editor: ICodeEditor): IInlineChatSession2 { + createSession(_editor: ICodeEditor): IInlineChatSession { throw new Error('Inline chat sessions are not supported in the sessions window'); } - getSessionByTextModel(_uri: URI): IInlineChatSession2 | undefined { + getSessionByTextModel(_uri: URI): IInlineChatSession | undefined { return undefined; } - getSessionBySessionUri(_uri: URI): IInlineChatSession2 | undefined { + getSessionBySessionUri(_uri: URI): IInlineChatSession | undefined { return undefined; } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 1463d518357d3..d5b7479cbf6c0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -29,7 +29,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js'; -import { reviewEdits } from '../../../inlineChat/browser/inlineChatController.js'; +import { reviewEdits } from './reviewEdits.js'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatCopyKind, IChatService } from '../../common/chatService/chatService.js'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index df32c865dff3b..347b30e943a7a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -29,7 +29,7 @@ import { IQuickInputService } from '../../../../../platform/quickinput/common/qu import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; -import { reviewEdits, reviewNotebookEdits } from '../../../inlineChat/browser/inlineChatController.js'; +import { reviewEdits, reviewNotebookEdits } from './reviewEdits.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, ICellEditOperation, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reviewEdits.ts b/src/vs/workbench/contrib/chat/browser/actions/reviewEdits.ts new file mode 100644 index 0000000000000..5fdad2d2915ba --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/reviewEdits.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { raceCancellation } from '../../../../../base/common/async.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { derived, waitForState } from '../../../../../base/common/observable.js'; +import { assertType } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; +import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../common/constants.js'; +import { ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; +import { ChatModel } from '../../common/model/chatModel.js'; +import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; + + +export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise { + if (!editor.hasModel()) { + return false; + } + + const chatService = accessor.get(IChatService); + const uri = editor.getModel().uri; + const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); + const chatModel = chatModelRef.object as ChatModel; + + chatModel.startEditingSession(true); + + const store = new DisposableStore(); + store.add(chatModelRef); + + // STREAM + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, { + kind: undefined, + modeId: 'applyCodeBlock', + modeInstructions: undefined, + isBuiltin: true, + applyCodeBlockSuggestionId, + }); + assertType(chatRequest.response); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); + for await (const chunk of stream) { + + if (token.isCancellationRequested) { + chatRequest.response.cancel(); + break; + } + + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); + } + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); + + if (!token.isCancellationRequested) { + chatRequest.response.complete(); + } + + const isSettled = derived(r => { + const entry = chatModel.editingSession?.readEntry(uri, r); + if (!entry) { + return false; + } + const state = entry.state.read(r); + return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; + }); + const whenDecided = waitForState(isSettled, Boolean); + await raceCancellation(whenDecided, token); + store.dispose(); + return true; +} + +export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, stream: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, token: CancellationToken): Promise { + + const chatService = accessor.get(IChatService); + const notebookService = accessor.get(INotebookService); + const isNotebook = notebookService.hasSupportedNotebooks(uri); + const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); + const chatModel = chatModelRef.object as ChatModel; + + chatModel.startEditingSession(true); + + const store = new DisposableStore(); + store.add(chatModelRef); + + // STREAM + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + if (isNotebook) { + chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: false }); + } else { + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); + } + for await (const chunk of stream) { + + if (token.isCancellationRequested) { + chatRequest.response.cancel(); + break; + } + if (chunk.every(isCellEditOperation)) { + chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: chunk, done: false }); + } else { + chatRequest.response.updateContent({ kind: 'textEdit', uri: chunk[0], edits: chunk[1], done: false }); + } + } + if (isNotebook) { + chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: true }); + } else { + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); + } + + if (!token.isCancellationRequested) { + chatRequest.response.complete(); + } + + const isSettled = derived(r => { + const entry = chatModel.editingSession?.readEntry(uri, r); + if (!entry) { + return false; + } + const state = entry.state.read(r); + return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; + }); + + const whenDecided = waitForState(isSettled, Boolean); + + await raceCancellation(whenDecided, token); + + store.dispose(); + + return true; +} +function isCellEditOperation(edit: URI | TextEdit[] | ICellEditOperation): edit is ICellEditOperation { + if (URI.isUri(edit)) { + return false; + } + if (Array.isArray(edit)) { + return false; + } + return true; +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 8073fa472f1f6..9822c1333c631 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -3,27 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './inlineChatDefaultModel.js'; - import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { InlineChatController } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; -import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatEscapeToolContribution, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; - import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { CancelAction, ChatSubmitAction } from '../../chat/browser/actions/chatExecuteActions.js'; import { localize } from '../../../../nls.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; +import { InlineChatDefaultModel } from './inlineChatDefaultModel.js'; registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors @@ -97,10 +93,10 @@ registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.FixDiagnosticsAction); registerAction2(InlineChatActions.DismissEditorAffordanceAction); - -const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); - +// --- contribs --- +registerWorkbenchContribution2('inlineChat.notebooks', InlineChatNotebookContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(InlineChatDefaultModel.ID, InlineChatDefaultModel, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(InlineChatEscapeToolContribution.Id, InlineChatEscapeToolContribution, WorkbenchPhase.AfterRestored); + AccessibleViewRegistry.register(new InlineChatAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 84974401e3f67..cb56e1c70e55b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -5,13 +5,10 @@ import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; -import { raceCancellation } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; import { autorun, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, waitForState } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { assertType } from '../../../../base/common/types.js'; @@ -23,24 +20,18 @@ import { IPosition, Position } from '../../../../editor/common/core/position.js' import { IRange, Range } from '../../../../editor/common/core/range.js'; import { ISelection, Selection } from '../../../../editor/common/core/selection.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; -import { TextEdit } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js'; -import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; -import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; -import { ChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; import { IChatService, IChatToolInvocation, ToolConfirmKind } from '../../chat/common/chatService/chatService.js'; import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../chat/common/attachments/chatVariableEntries.js'; @@ -49,12 +40,10 @@ import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsService, isILanguageModelChatSelector } from '../../chat/common/languageModels.js'; import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; -import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; -import { INotebookService } from '../../notebook/common/notebookService.js'; +import { CellUri } from '../../notebook/common/notebookCommon.js'; import { CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_TERMINATED, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; import { InlineChatAffordance } from './inlineChatAffordance.js'; - -import { continueInPanelChat, IInlineChatSession2, IInlineChatSessionService, rephraseInlineChat } from './inlineChatSessionService.js'; +import { continueInPanelChat, IInlineChatSession, IInlineChatSessionService, rephraseInlineChat } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; @@ -119,16 +108,13 @@ export class InlineChatController implements IEditorContribution { readonly #zone: Lazy; readonly inputOverlayWidget: InlineChatAffordance; - readonly #currentSession: IObservable; + readonly #currentSession: IObservable; readonly #editor: ICodeEditor; readonly #instaService: IInstantiationService; readonly #notebookEditorService: INotebookEditorService; readonly #inlineChatSessionService: IInlineChatSessionService; readonly #configurationService: IConfigurationService; - readonly #webContentExtractorService: ISharedWebContentExtractorService; - readonly #fileService: IFileService; - readonly #chatAttachmentResolveService: IChatAttachmentResolveService; readonly #editorService: IEditorService; readonly #markerDecorationsService: IMarkerDecorationsService; readonly #languageModelService: ILanguageModelsService; @@ -152,9 +138,6 @@ export class InlineChatController implements IEditorContribution { @ICodeEditorService codeEditorService: ICodeEditorService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, - @ISharedWebContentExtractorService webContentExtractorService: ISharedWebContentExtractorService, - @IFileService fileService: IFileService, - @IChatAttachmentResolveService chatAttachmentResolveService: IChatAttachmentResolveService, @IEditorService editorService: IEditorService, @IMarkerDecorationsService markerDecorationsService: IMarkerDecorationsService, @ILanguageModelsService languageModelService: ILanguageModelsService, @@ -167,9 +150,6 @@ export class InlineChatController implements IEditorContribution { this.#notebookEditorService = notebookEditorService; this.#inlineChatSessionService = inlineChatSessionService; this.#configurationService = configurationService; - this.#webContentExtractorService = webContentExtractorService; - this.#fileService = fileService; - this.#chatAttachmentResolveService = chatAttachmentResolveService; this.#editorService = editorService; this.#markerDecorationsService = markerDecorationsService; this.#languageModelService = languageModelService; @@ -182,7 +162,7 @@ export class InlineChatController implements IEditorContribution { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); const ctxTerminated = CTX_INLINE_CHAT_TERMINATED.bindTo(contextKeyService); - const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this.#configurationService); + const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.NotebookAgent, false, this.#configurationService); // Track whether the current editor's file is being edited by any chat editing session this.#store.add(autorun(r => { @@ -290,7 +270,7 @@ export class InlineChatController implements IEditorContribution { }); - let lastSession: IInlineChatSession2 | undefined = undefined; + let lastSession: IInlineChatSession | undefined = undefined; this.#store.add(autorun(r => { const session = this.#currentSession.read(r); @@ -322,7 +302,7 @@ export class InlineChatController implements IEditorContribution { } })); - const visibleSessionObs = observableValue(this, undefined); + const visibleSessionObs = observableValue(this, undefined); this.#store.add(autorun(r => { @@ -541,7 +521,7 @@ export class InlineChatController implements IEditorContribution { /** * Zone mode: use the full zone widget and chat widget for request submission. */ - async #runZone(session: IInlineChatSession2, arg?: InlineChatRunOptions): Promise { + async #runZone(session: IInlineChatSession, arg?: InlineChatRunOptions): Promise { assertType(this.#editor.hasModel()); const uri = this.#editor.getModel().uri; @@ -671,7 +651,7 @@ export class InlineChatController implements IEditorContribution { this.#zone.rawValue?.widget.focus(); } - async #selectVendorDefaultModel(session: IInlineChatSession2): Promise { + async #selectVendorDefaultModel(session: IInlineChatSession): Promise { const model = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.get(); if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { const ids = await this.#languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); @@ -689,7 +669,7 @@ export class InlineChatController implements IEditorContribution { * Applies model defaults based on settings and tracks user model changes. * Prioritization: user session choice > inlineChat.defaultModel setting > vendor default */ - async #applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { + async #applyModelDefaults(session: IInlineChatSession, sessionStore: DisposableStore): Promise { const userSelectedModel = InlineChatController.#userSelectedModel; const defaultModelSetting = this.#configurationService.getValue(InlineChatConfigKeys.DefaultModel); @@ -736,147 +716,4 @@ export class InlineChatController implements IEditorContribution { } })); } - - async createImageAttachment(attachment: URI): Promise { - const value = this.#currentSession.get(); - if (!value) { - return undefined; - } - if (attachment.scheme === Schemas.file) { - if (await this.#fileService.canHandleResource(attachment)) { - return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); - } - } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { - const extractedImages = await this.#webContentExtractorService.readImage(attachment, CancellationToken.None); - if (extractedImages) { - return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); - } - } - return undefined; - } -} - -export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise { - if (!editor.hasModel()) { - return false; - } - - const chatService = accessor.get(IChatService); - const uri = editor.getModel().uri; - const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); - const chatModel = chatModelRef.object as ChatModel; - - chatModel.startEditingSession(true); - - const store = new DisposableStore(); - store.add(chatModelRef); - - // STREAM - const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, { - kind: undefined, - modeId: 'applyCodeBlock', - modeInstructions: undefined, - isBuiltin: true, - applyCodeBlockSuggestionId, - }); - assertType(chatRequest.response); - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); - for await (const chunk of stream) { - - if (token.isCancellationRequested) { - chatRequest.response.cancel(); - break; - } - - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); - } - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - - if (!token.isCancellationRequested) { - chatRequest.response.complete(); - } - - const isSettled = derived(r => { - const entry = chatModel.editingSession?.readEntry(uri, r); - if (!entry) { - return false; - } - const state = entry.state.read(r); - return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; - }); - const whenDecided = waitForState(isSettled, Boolean); - await raceCancellation(whenDecided, token); - store.dispose(); - return true; -} - -export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, stream: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, token: CancellationToken): Promise { - - const chatService = accessor.get(IChatService); - const notebookService = accessor.get(INotebookService); - const isNotebook = notebookService.hasSupportedNotebooks(uri); - const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); - const chatModel = chatModelRef.object as ChatModel; - - chatModel.startEditingSession(true); - - const store = new DisposableStore(); - store.add(chatModelRef); - - // STREAM - const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); - assertType(chatRequest.response); - if (isNotebook) { - chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: false }); - } else { - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); - } - for await (const chunk of stream) { - - if (token.isCancellationRequested) { - chatRequest.response.cancel(); - break; - } - if (chunk.every(isCellEditOperation)) { - chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: chunk, done: false }); - } else { - chatRequest.response.updateContent({ kind: 'textEdit', uri: chunk[0], edits: chunk[1], done: false }); - } - } - if (isNotebook) { - chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: true }); - } else { - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - } - - if (!token.isCancellationRequested) { - chatRequest.response.complete(); - } - - const isSettled = derived(r => { - const entry = chatModel.editingSession?.readEntry(uri, r); - if (!entry) { - return false; - } - const state = entry.state.read(r); - return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; - }); - - const whenDecided = waitForState(isSettled, Boolean); - - await raceCancellation(whenDecided, token); - - store.dispose(); - - return true; -} - -function isCellEditOperation(edit: URI | TextEdit[] | ICellEditOperation): edit is ICellEditOperation { - if (URI.isUri(edit)) { - return false; - } - if (Array.isArray(edit)) { - return false; - } - return true; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts index 5fd53270237b5..29ec10f5272b9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts @@ -7,7 +7,6 @@ import { localize } from '../../../../nls.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ILanguageModelsService } from '../../chat/common/languageModels.js'; import { InlineChatConfigKeys } from '../common/inlineChat.js'; import { createDefaultModelArrays, DefaultModelContribution } from '../../chat/browser/defaultModelContribution.js'; @@ -34,7 +33,6 @@ export class InlineChatDefaultModel extends DefaultModelContribution { } } -registerWorkbenchContribution2(InlineChatDefaultModel.ID, InlineChatDefaultModel, WorkbenchPhase.BlockRestore); Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ ...{ id: 'inlineChat', title: localize('inlineChatConfigurationTitle', 'Inline Chat'), order: 30, type: 'object' }, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts index ca722843a32e6..bf1d2ea10aa18 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts @@ -10,7 +10,6 @@ import { InlineChatController } from './inlineChatController.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri } from '../../notebook/common/notebookCommon.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; export class InlineChatNotebookContribution { @@ -18,7 +17,6 @@ export class InlineChatNotebookContribution { constructor( @IInlineChatSessionService sessionService: IInlineChatSessionService, - @IEditorService editorService: IEditorService, @INotebookEditorService notebookEditorService: INotebookEditorService, ) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 8a348c9fba5a2..1fb914877cf01 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -21,7 +21,7 @@ export const IInlineChatSessionService = createDecorator; readonly onDidChangeSessions: Event; - dispose(): void; - - createSession(editor: ICodeEditor): IInlineChatSession2; - getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined; - getSessionBySessionUri(uri: URI): IInlineChatSession2 | undefined; -} - -export async function moveToPanelChat(accessor: ServicesAccessor, model: IChatModel | undefined, resend: boolean) { - - const chatService = accessor.get(IChatService); - const widgetService = accessor.get(IChatWidgetService); - - const widget = await widgetService.revealWidget(); - - if (widget && widget.viewModel && model) { - let lastRequest: IChatRequestModel | undefined; - for (const request of model.getRequests().slice()) { - await chatService.adoptRequest(widget.viewModel.model.sessionResource, request); - lastRequest = request; - } - - if (lastRequest && resend) { - chatService.resendRequest(lastRequest, { location: widget.location }); - } - - widget.focusResponseItem(); - } + createSession(editor: ICodeEditor): IInlineChatSession; + getSessionByTextModel(uri: URI): IInlineChatSession | undefined; + getSessionBySessionUri(uri: URI): IInlineChatSession | undefined; } -export async function askInPanelChat(accessor: ServicesAccessor, request: IChatRequestModel, state: IChatModelInputState | undefined, fileContext?: { uri: URI; selection: Selection }) { +async function askInPanelChat(accessor: ServicesAccessor, request: IChatRequestModel, state: IChatModelInputState | undefined, fileContext?: { uri: URI; selection: Selection }) { const widgetService = accessor.get(IChatWidgetService); const chatService = accessor.get(IChatService); @@ -94,7 +70,7 @@ export async function askInPanelChat(accessor: ServicesAccessor, request: IChatR widget?.acceptInput(request.message.text); } -export async function continueInPanelChat(accessor: ServicesAccessor, session: IInlineChatSession2): Promise { +export async function continueInPanelChat(accessor: ServicesAccessor, session: IInlineChatSession): Promise { const request = session.chatModel.getRequests().at(-1); if (!request) { return; @@ -104,7 +80,7 @@ export async function continueInPanelChat(accessor: ServicesAccessor, session: I session.dispose(); } -export function rephraseInlineChat(accessor: ServicesAccessor, session: IInlineChatSession2): string | undefined { +export function rephraseInlineChat(accessor: ServicesAccessor, session: IInlineChatSession): string | undefined { const request = session.chatModel.getRequests().at(-1); if (!request) { return undefined; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index e18149801cba7..3c9d7cb8cc430 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -21,7 +21,7 @@ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../chat/common/tools/languageModelToolsService.js'; import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { IInlineChatSession2, IInlineChatSessionService, InlineChatSessionTerminationState } from './inlineChatSessionService.js'; +import { IInlineChatSession, IInlineChatSessionService, InlineChatSessionTerminationState } from './inlineChatSessionService.js'; export class InlineChatError extends Error { static readonly code = 'InlineChatError'; @@ -36,7 +36,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { declare _serviceBrand: undefined; readonly #store = new DisposableStore(); - readonly #sessions = new ResourceMap(); + readonly #sessions = new ResourceMap(); readonly #onWillStartSession = this.#store.add(new Emitter()); readonly onWillStartSession: Event = this.#onWillStartSession.event; @@ -68,7 +68,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } - createSession(editor: IActiveCodeEditor): IInlineChatSession2 { + createSession(editor: IActiveCodeEditor): IInlineChatSession { const uri = editor.getModel().uri; if (this.#sessions.has(uri)) { @@ -128,7 +128,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } })); - const result: IInlineChatSession2 = { + const result: IInlineChatSession = { uri, initialPosition: editor.getSelection().getStartPosition().delta(-1), /* one line above selection start */ initialSelection: editor.getSelection(), @@ -146,7 +146,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { return result; } - getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined { + getSessionByTextModel(uri: URI): IInlineChatSession | undefined { let result = this.#sessions.get(uri); if (!result) { // no direct session, try to find an editing session which has a file entry for the uri @@ -161,7 +161,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { return result; } - getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined { + getSessionBySessionUri(sessionResource: URI): IInlineChatSession | undefined { for (const session of this.#sessions.values()) { if (isEqual(session.chatModel.sessionResource, sessionResource)) { return session; @@ -193,7 +193,7 @@ export class InlineChatEnabler { const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook)); - const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService); + const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.NotebookAgent, false, configService); this.#store.add(autorun(r => { const agent = agentObs.read(r); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5fd88f1448453..1ee9a1f61f2a1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -14,10 +14,7 @@ import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; import { ICodeEditorViewState } from '../../../../editor/common/editorCommon.js'; -import { ITextModel } from '../../../../editor/common/model.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../nls.js'; import { IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; @@ -44,7 +41,6 @@ import { IChatWidgetViewOptions } from '../../chat/browser/chat.js'; import { ChatWidget, IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; import { chatRequestBackground } from '../../chat/common/widget/chatColors.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; -import { IChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; import { ChatAgentVoteDirection, IChatService } from '../../chat/common/chatService/chatService.js'; import { isResponseVM } from '../../chat/common/model/chatViewModel.js'; @@ -74,7 +70,7 @@ export interface IInlineChatWidgetConstructionOptions { inZoneWidget?: boolean; } -export class InlineChatWidget { +export abstract class InlineChatWidget { protected readonly _elements = h( 'div.inline-chat@root', @@ -96,7 +92,7 @@ export class InlineChatWidget { readonly #ctxInputEditorFocused: IContextKey; readonly #ctxResponseFocused: IContextKey; - readonly #chatWidget: ChatWidget; + readonly chatWidget: ChatWidget; protected readonly _onDidChangeHeight = this._store.add(new Emitter()); readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this.#isLayouting); @@ -113,7 +109,6 @@ export class InlineChatWidget { readonly #accessibilityService: IAccessibilityService; readonly #configurationService: IConfigurationService; readonly #accessibleViewService: IAccessibleViewService; - readonly #modelService: IModelService; readonly #chatService: IChatService; readonly #chatEntitlementService: IChatEntitlementService; readonly #markdownRendererService: IMarkdownRendererService; @@ -128,7 +123,6 @@ export class InlineChatWidget { @IConfigurationService configurationService: IConfigurationService, @IAccessibleViewService accessibleViewService: IAccessibleViewService, @ITextModelService protected readonly _textModelResolverService: ITextModelService, - @IModelService modelService: IModelService, @IChatService chatService: IChatService, @IHoverService hoverService: IHoverService, @IChatEntitlementService chatEntitlementService: IChatEntitlementService, @@ -139,7 +133,6 @@ export class InlineChatWidget { this.#accessibilityService = accessibilityService; this.#configurationService = configurationService; this.#accessibleViewService = accessibleViewService; - this.#modelService = modelService; this.#chatService = chatService; this.#chatEntitlementService = chatEntitlementService; this.#markdownRendererService = markdownRendererService; @@ -153,7 +146,7 @@ export class InlineChatWidget { this._store ); - this.#chatWidget = scopedInstaService.createInstance( + this.chatWidget = scopedInstaService.createInstance( ChatWidget, location, { isInlineChat: true }, @@ -191,10 +184,10 @@ export class InlineChatWidget { } ); this._elements.root.classList.toggle('in-zone-widget', !!options.inZoneWidget); - this.#chatWidget.render(this._elements.chatWidget); + this.chatWidget.render(this._elements.chatWidget); this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground)); - this.#chatWidget.setVisible(true); - this._store.add(this.#chatWidget); + this.chatWidget.setVisible(true); + this._store.add(this.chatWidget); const ctxResponse = ChatContextKeys.isResponse.bindTo(this.scopedContextKeyService); const ctxResponseVote = ChatContextKeys.responseVote.bindTo(this.scopedContextKeyService); @@ -203,10 +196,10 @@ export class InlineChatWidget { const ctxResponseErrorFiltered = ChatContextKeys.responseIsFiltered.bindTo(this.scopedContextKeyService); const viewModelStore = this._store.add(new DisposableStore()); - this._store.add(this.#chatWidget.onDidChangeViewModel(() => { + this._store.add(this.chatWidget.onDidChangeViewModel(() => { viewModelStore.clear(); - const viewModel = this.#chatWidget.viewModel; + const viewModel = this.chatWidget.viewModel; if (!viewModel) { return; } @@ -249,8 +242,8 @@ export class InlineChatWidget { this._store.add(tracker.onDidFocus(() => this.#ctxResponseFocused.set(true))); this.#ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(contextKeyService); - this._store.add(this.#chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true))); - this._store.add(this.#chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false))); + this._store.add(this.chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true))); + this._store.add(this.chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false))); const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; @@ -293,7 +286,7 @@ export class InlineChatWidget { })); this._store.add(this.#chatService.onDidPerformUserAction(e => { - if (isEqual(e.sessionResource, this.#chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { + if (isEqual(e.sessionResource, this.chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { this.updateStatus(localize('feedbackThanks', "Thank you for your feedback!"), { resetAfter: 1250 }); } })); @@ -311,7 +304,7 @@ export class InlineChatWidget { ? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel) : localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information."); } - this.#chatWidget.inputEditor.updateOptions({ ariaLabel: label }); + this.chatWidget.inputEditor.updateOptions({ ariaLabel: label }); } } @@ -346,14 +339,6 @@ export class InlineChatWidget { return this._elements.root; } - get chatWidget(): ChatWidget { - return this.#chatWidget; - } - - saveState() { - this.#chatWidget.saveState(); - } - layout(widgetDim: Dimension) { const contentHeight = this.contentHeight; this.#isLayouting = true; @@ -377,7 +362,7 @@ export class InlineChatWidget { this._elements.root.style.height = `${dimension.height - extraHeight}px`; this._elements.root.style.width = `${dimension.width}px`; - this.#chatWidget.layout( + this.chatWidget.layout( dimension.height - statusHeight - extraHeight, dimension.width ); @@ -388,7 +373,7 @@ export class InlineChatWidget { */ get contentHeight(): number { const data = { - chatWidgetContentHeight: this.#chatWidget.contentHeight, + chatWidgetContentHeight: this.chatWidget.contentHeight, statusHeight: getTotalHeight(this._elements.status), extraHeight: this._getExtraHeight() }; @@ -401,7 +386,7 @@ export class InlineChatWidget { // at least "maxWidgetHeight" high and at most the content height. let maxWidgetOutputHeight = 100; - for (const item of this.#chatWidget.viewModel?.getItems() ?? []) { + for (const item of this.chatWidget.viewModel?.getItems() ?? []) { if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) { maxWidgetOutputHeight = 270; break; @@ -409,8 +394,8 @@ export class InlineChatWidget { } let value = this.contentHeight; - value -= this.#chatWidget.contentHeight; - value += Math.min(this.#chatWidget.input.height.get() + maxWidgetOutputHeight, this.#chatWidget.contentHeight); + value -= this.chatWidget.contentHeight; + value += Math.min(this.chatWidget.input.height.get() + maxWidgetOutputHeight, this.chatWidget.contentHeight); return value; } @@ -418,100 +403,6 @@ export class InlineChatWidget { return this.#options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/); } - get value(): string { - return this.#chatWidget.getInput(); - } - - set value(value: string) { - this.#chatWidget.setInput(value); - } - - selectAll() { - this.#chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); - } - - set placeholder(value: string) { - this.#chatWidget.setInputPlaceholder(value); - } - - toggleStatus(show: boolean) { - this._elements.toolbar1.classList.toggle('hidden', !show); - this._elements.toolbar2.classList.toggle('hidden', !show); - this._elements.status.classList.toggle('hidden', !show); - this._elements.infoLabel.classList.toggle('hidden', !show); - this._onDidChangeHeight.fire(); - } - - updateToolbar(show: boolean) { - this._elements.root.classList.toggle('toolbar', show); - this._elements.toolbar1.classList.toggle('hidden', !show); - this._elements.toolbar2.classList.toggle('hidden', !show); - this._elements.status.classList.toggle('actions', show); - this._elements.infoLabel.classList.toggle('hidden', show); - this._onDidChangeHeight.fire(); - } - - async getCodeBlockInfo(codeBlockIndex: number): Promise { - const { viewModel } = this.#chatWidget; - if (!viewModel) { - return undefined; - } - const items = viewModel.getItems().filter(i => isResponseVM(i)); - const item = items.at(-1); - if (!item) { - return; - } - - // Look for the code block in the rendered response - const codeBlocks = this.#chatWidget.getCodeBlockInfosForResponse(item); - const info = codeBlocks[codeBlockIndex]; - if (info?.uri) { - return this.#modelService.getModel(info.uri) ?? undefined; - } - - // Fallback: if the code block hasn't been rendered yet (e.g. due to - // timing between response completion and list rendering), parse the - // markdown directly and create a transient model. - const markdown = item.response.getMarkdown(); - let currentCodeBlockIndex = 0; - let foundText: string | undefined; - - for (const line of markdown.split('\n')) { - if (line.startsWith('```') && foundText === undefined) { - foundText = ''; - } else if (line.startsWith('```') && foundText !== undefined) { - if (currentCodeBlockIndex === codeBlockIndex) { - break; - } - currentCodeBlockIndex++; - foundText = undefined; - } else if (foundText !== undefined) { - foundText += (foundText ? '\n' : '') + line; - } - } - - if (foundText !== undefined && currentCodeBlockIndex === codeBlockIndex) { - return this.#modelService.createModel(foundText, null, undefined, true); - } - - return undefined; - } - - get responseContent(): string | undefined { - const requests = this.#chatWidget.viewModel?.model.getRequests(); - return requests?.at(-1)?.response?.response.toString(); - } - - - getChatModel(): IChatModel | undefined { - return this.#chatWidget.viewModel?.model; - } - - setChatModel(chatModel: IChatModel) { - chatModel.inputModel.setState({ inputText: '', selections: [] }); - this.#chatWidget.setModel(chatModel); - } - updateInfo(message: string): void { this._elements.infoLabel.classList.toggle('hidden', !message); const renderedMessage = renderLabelWithIcons(message); @@ -548,8 +439,8 @@ export class InlineChatWidget { } reset() { - this.#chatWidget.attachmentModel.clear(true); - this.#chatWidget.saveState(); + this.chatWidget.attachmentModel.clear(true); + this.chatWidget.saveState(); reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); @@ -562,7 +453,7 @@ export class InlineChatWidget { } focus() { - this.#chatWidget.focusInput(); + this.chatWidget.focusInput(); } hasFocus() { @@ -586,7 +477,6 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IConfigurationService configurationService: IConfigurationService, @IAccessibleViewService accessibleViewService: IAccessibleViewService, @ITextModelService textModelResolverService: ITextModelService, - @IModelService modelService: IModelService, @IChatService chatService: IChatService, @IHoverService hoverService: IHoverService, @ILayoutService layoutService: ILayoutService, @@ -600,7 +490,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { ...options.chatWidgetViewOptions, editorOverflowWidgetsDomNode: overflowWidgetsNode } - }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, modelService, chatService, hoverService, chatEntitlementService, markdownRendererService); + }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService, chatEntitlementService, markdownRendererService); this._store.add(toDisposable(() => { overflowWidgetsNode.remove(); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 9a05e4cec1097..689a0c6d01eb5 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -14,10 +14,9 @@ import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../notebook/common/notebookContext // settings export const enum InlineChatConfigKeys { - FinishOnType = 'inlineChat.finishOnType', /** @deprecated do not read on client */ EnableV2 = 'inlineChat.enableV2', - notebookAgent = 'inlineChat.notebookAgent', + NotebookAgent = 'inlineChat.notebookAgent', DefaultModel = 'inlineChat.defaultModel', Affordance = 'inlineChat.affordance', FixDiagnostics = 'inlineChat.fixDiagnostics', @@ -27,11 +26,6 @@ export const enum InlineChatConfigKeys { Registry.as(Extensions.Configuration).registerConfiguration({ id: 'editor', properties: { - [InlineChatConfigKeys.FinishOnType]: { - description: localize('finishOnType', "Whether to finish an inline chat session when typing outside of changed regions."), - default: false, - type: 'boolean' - }, [InlineChatConfigKeys.EnableV2]: { description: localize('enableV2', "Whether to use the next version of inline chat."), default: false, @@ -41,7 +35,7 @@ Registry.as(Extensions.Configuration).registerConfigurat mode: 'auto' } }, - [InlineChatConfigKeys.notebookAgent]: { + [InlineChatConfigKeys.NotebookAgent]: { markdownDescription: localize('notebookAgent', "Enable agent-like behavior for inline chat widget in notebooks."), default: false, type: 'boolean', diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index be4f138c4eaf1..6e6730635d576 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -18,16 +18,29 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IChatAcceptInputOptions, IChatWidgetService } from '../../../chat/browser/chat.js'; import { IChatAgentService } from '../../../chat/common/participants/chatAgents.js'; -import { IChatResponseModel, isCellTextEditOperationArray } from '../../../chat/common/model/chatModel.js'; +import { IChatModel, IChatResponseModel, isCellTextEditOperationArray } from '../../../chat/common/model/chatModel.js'; import { ChatMode } from '../../../chat/common/chatModes.js'; import { IChatModelReference, IChatProgress, IChatService } from '../../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; -import { InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; +import { IInlineChatWidgetConstructionOptions, InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; import { ITerminalInstance, type IXtermTerminal } from '../../../terminal/browser/terminal.js'; import { TerminalStickyScrollContribution } from '../../stickyScroll/browser/terminalStickyScrollContribution.js'; import './media/terminalChatWidget.css'; import { MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from './terminalChat.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { isResponseVM } from '../../../chat/common/model/chatViewModel.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; +import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; +import { IChatWidgetLocationOptions } from '../../../chat/browser/widget/chatWidget.js'; +import { Selection } from '../../../../../editor/common/core/selection.js'; const enum Constants { HorizontalMargin = 10, @@ -58,8 +71,8 @@ export class TerminalChatWidget extends Disposable { private readonly _onDidHide = this._register(new Emitter()); readonly onDidHide = this._onDidHide.event; - private readonly _inlineChatWidget: InlineChatWidget; - public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } + private readonly _inlineChatWidget: TerminalInlineChatWidget; + public get inlineChatWidget(): TerminalInlineChatWidget { return this._inlineChatWidget; } private readonly _focusTracker: IFocusTracker; @@ -116,7 +129,7 @@ export class TerminalChatWidget extends Disposable { this._terminalElement.appendChild(this._container); this._inlineChatWidget = instantiationService.createInstance( - InlineChatWidget, + TerminalInlineChatWidget, { location: ChatAgentLocation.Terminal, resolveData: () => { @@ -466,3 +479,120 @@ export class TerminalChatWidget extends Disposable { this.hide(); } } + + +class TerminalInlineChatWidget extends InlineChatWidget { + + + constructor( + location: IChatWidgetLocationOptions, + options: IInlineChatWidgetConstructionOptions, + @IInstantiationService instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IConfigurationService configurationService: IConfigurationService, + @IAccessibleViewService accessibleViewService: IAccessibleViewService, + @ITextModelService textModelResolverService: ITextModelService, + @IChatService chatService: IChatService, + @IHoverService hoverService: IHoverService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService, + @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, + @IModelService private _modelService: IModelService, + ) { + super(location, options, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService, chatEntitlementService, markdownRendererService); + } + + get value(): string { + return this.chatWidget.getInput(); + } + + set value(value: string) { + this.chatWidget.setInput(value); + } + + selectAll() { + this.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + } + + set placeholder(value: string) { + this.chatWidget.setInputPlaceholder(value); + } + + toggleStatus(show: boolean) { + this._elements.toolbar1.classList.toggle('hidden', !show); + this._elements.toolbar2.classList.toggle('hidden', !show); + this._elements.status.classList.toggle('hidden', !show); + this._elements.infoLabel.classList.toggle('hidden', !show); + this._onDidChangeHeight.fire(); + } + + updateToolbar(show: boolean) { + this._elements.root.classList.toggle('toolbar', show); + this._elements.toolbar1.classList.toggle('hidden', !show); + this._elements.toolbar2.classList.toggle('hidden', !show); + this._elements.status.classList.toggle('actions', show); + this._elements.infoLabel.classList.toggle('hidden', show); + this._onDidChangeHeight.fire(); + } + + get responseContent(): string | undefined { + const requests = this.chatWidget.viewModel?.model.getRequests(); + return requests?.at(-1)?.response?.response.toString(); + } + + getChatModel(): IChatModel | undefined { + return this.chatWidget.viewModel?.model; + } + + setChatModel(chatModel: IChatModel) { + chatModel.inputModel.setState({ inputText: '', selections: [] }); + this.chatWidget.setModel(chatModel); + } + + async getCodeBlockInfo(codeBlockIndex: number): Promise { + const { viewModel } = this.chatWidget; + if (!viewModel) { + return undefined; + } + const items = viewModel.getItems().filter(i => isResponseVM(i)); + const item = items.at(-1); + if (!item) { + return; + } + + // Look for the code block in the rendered response + const codeBlocks = this.chatWidget.getCodeBlockInfosForResponse(item); + const info = codeBlocks[codeBlockIndex]; + if (info?.uri) { + return this._modelService.getModel(info.uri) ?? undefined; + } + + // Fallback: if the code block hasn't been rendered yet (e.g. due to + // timing between response completion and list rendering), parse the + // markdown directly and create a transient model. + const markdown = item.response.getMarkdown(); + let currentCodeBlockIndex = 0; + let foundText: string | undefined; + + for (const line of markdown.split('\n')) { + if (line.startsWith('```') && foundText === undefined) { + foundText = ''; + } else if (line.startsWith('```') && foundText !== undefined) { + if (currentCodeBlockIndex === codeBlockIndex) { + break; + } + currentCodeBlockIndex++; + foundText = undefined; + } else if (foundText !== undefined) { + foundText += (foundText ? '\n' : '') + line; + } + } + + if (foundText !== undefined && currentCodeBlockIndex === codeBlockIndex) { + return this._modelService.createModel(foundText, null, undefined, true); + } + + return undefined; + } +} From 3981b5d4fd52539c8fd978ccd9a0613191d1855e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:57:43 +0000 Subject: [PATCH 17/33] Agents - improve GitHub model handling (#312342) * Agents - improve keeping track of polling * Better handle multiple refresh calls * Pull request feedback --- .../models/githubPullRequestCIModel.ts | 41 ++++++++++++++++--- .../browser/models/githubPullRequestModel.ts | 41 +++++++++++++++---- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts b/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts index 3f0cec668525e..1c14f56eb151b 100644 --- a/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts +++ b/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts @@ -25,7 +25,10 @@ export class GitHubPullRequestCIModel extends Disposable { private readonly _overallStatus = observableValue(this, GitHubCIOverallStatus.Neutral); readonly overallStatus: IObservable = this._overallStatus; + private _pollingClients = 0; + private _refreshPromise: Promise | undefined; private readonly _pollScheduler: RunOnceScheduler; + private _disposed = false; constructor( @@ -43,7 +46,19 @@ export class GitHubPullRequestCIModel extends Disposable { /** * Refresh all CI check data. */ - async refresh(): Promise { + refresh(): Promise { + if (!this._refreshPromise) { + this._refreshPromise = this._refresh().finally(() => { + if (this._refreshPromise) { + this._refreshPromise = undefined; + } + }); + } + + return this._refreshPromise; + } + + private async _refresh(): Promise { try { const checks = await this._fetcher.getCheckRuns(this.owner, this.repo, this.headRef); this._checks.set(checks, undefined); @@ -78,27 +93,43 @@ export class GitHubPullRequestCIModel extends Disposable { * Start periodic polling. Each cycle refreshes CI check data. */ startPolling(intervalMs: number = DEFAULT_POLL_INTERVAL_MS): void { - this._pollScheduler.cancel(); - this._pollScheduler.schedule(intervalMs); + this._pollScheduler.delay = intervalMs; + + this._pollingClients++; + if (this._pollingClients === 1) { + this._pollScheduler.cancel(); + this._pollScheduler.schedule(); + } } /** * Stop periodic polling. */ stopPolling(): void { - this._pollScheduler.cancel(); + if (this._pollingClients === 0) { + return; + } + + this._pollingClients--; + if (this._pollingClients === 0) { + this._pollScheduler.cancel(); + } } private async _poll(): Promise { await this.refresh(); + // Re-schedule if not disposed (RunOnceScheduler is one-shot) - if (!this._disposed) { + if (!this._disposed && this._pollingClients > 0) { this._pollScheduler.schedule(); } } override dispose(): void { this._disposed = true; + this._pollingClients = 0; + this._refreshPromise = undefined; + super.dispose(); } } diff --git a/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts b/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts index 0af71dfbbe929..510e4811ab6b6 100644 --- a/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts +++ b/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts @@ -28,7 +28,10 @@ export class GitHubPullRequestModel extends Disposable { private readonly _reviewThreads = observableValue(this, []); readonly reviewThreads: IObservable = this._reviewThreads; + private _pollingClients = 0; + private _refreshPromise: Promise | undefined; private readonly _pollScheduler: RunOnceScheduler; + private _disposed = false; constructor( @@ -46,8 +49,16 @@ export class GitHubPullRequestModel extends Disposable { /** * Refresh all PR data: pull request info, mergeability, and review threads. */ - async refresh(): Promise { - await this._refresh(); + refresh(): Promise { + if (!this._refreshPromise) { + this._refreshPromise = this._refresh().finally(() => { + if (this._refreshPromise) { + this._refreshPromise = undefined; + } + }); + } + + return this._refreshPromise; } /** @@ -80,23 +91,34 @@ export class GitHubPullRequestModel extends Disposable { * Start periodic polling. Each cycle refreshes all PR data. */ startPolling(intervalMs: number = DEFAULT_POLL_INTERVAL_MS): void { - this._pollScheduler.cancel(); - this._pollScheduler.schedule(intervalMs); + this._pollScheduler.delay = intervalMs; + + this._pollingClients++; + if (this._pollingClients === 1) { + this._pollScheduler.cancel(); + this._pollScheduler.schedule(); + } } /** * Stop periodic polling. */ stopPolling(): void { - this._pollScheduler.cancel(); + if (this._pollingClients === 0) { + return; + } + + this._pollingClients--; + if (this._pollingClients === 0) { + this._pollScheduler.cancel(); + } } private async _poll(): Promise { await this.refresh(); - // Re-schedule for next poll cycle - // as RunOnceScheduler is one-shot - if (!this._disposed) { + // Re-schedule if not disposed (RunOnceScheduler is one-shot) + if (!this._disposed && this._pollingClients > 0) { this._pollScheduler.schedule(); } } @@ -117,6 +139,9 @@ export class GitHubPullRequestModel extends Disposable { override dispose(): void { this._disposed = true; + this._pollingClients = 0; + this._refreshPromise = undefined; + super.dispose(); } } From 4b8f34ef7e433e849fc71fa4857cdac8a8314d79 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 24 Apr 2026 07:08:01 -0700 Subject: [PATCH 18/33] Preserve focus when opening browser with non-agentic tool (#312344) --- .../electron-browser/tools/openBrowserToolNonAgentic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts index 149fdd44c796a..c280c2955482a 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts @@ -63,7 +63,7 @@ export class OpenBrowserToolNonAgentic implements IToolImpl { logBrowserOpen(this.telemetryService, 'chatTool'); const browserUri = BrowserViewUri.forId(generateUuid()); - await this.editorService.openEditor({ resource: browserUri, options: { pinned: true, viewState: { url: params.url } } }); + await this.editorService.openEditor({ resource: browserUri, options: { pinned: true, preserveFocus: true, viewState: { url: params.url } } }); return { content: [{ From c140f4fd5157f7eba40c69dd7b9cb0bbf33b0bee Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 24 Apr 2026 16:16:55 +0200 Subject: [PATCH 19/33] refactor: remove unnecessary assignees from classifier.json (#312345) --- .github/classifier.json | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/classifier.json b/.github/classifier.json index 39ebd9e38b222..21a1f237181d4 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -6,24 +6,24 @@ }, "labels": { "accessibility": { "assign": ["meganrogge"]}, - "api": {"assign": ["jrieken"]}, + "api": {"assign": []}, "api-finalization": {"assign": []}, - "api-proposal": {"assign": ["jrieken"]}, + "api-proposal": {"assign": []}, "authentication": {"assign": ["TylerLeonhardt"]}, - "bisect-ext": {"assign": ["jrieken"]}, + "bisect-ext": {"assign": []}, "bot-proposal": {"assign": ["lramos15"]}, "bracket-pair-colorization": {"assign": ["hediet"]}, "bracket-pair-guides": {"assign": ["hediet"]}, - "breadcrumbs": {"assign": ["jrieken"]}, - "callhierarchy": {"assign": ["jrieken"]}, + "breadcrumbs": {"assign": []}, + "callhierarchy": {"assign": []}, "chat-terminal": {"assign": ["meganrogge"]}, "chat-terminal-output-monitor": {"assign": ["meganrogge"]}, "chrome-devtools": {"assign": ["deepak1556"]}, "cloud-changes": {"assign": ["joyceerhl"]}, "code-cli": {"assign": ["connor4312"]}, - "code-lens": {"assign": ["jrieken"]}, + "code-lens": {"assign": []}, "code-server-web": {"assign": ["aeschli"]}, - "command-center": {"assign": ["jrieken"]}, + "command-center": {"assign": []}, "comments": {"assign": ["alexr00"]}, "config": {"assign": ["sandy081"]}, "containers": {"assign": ["chrmarti"]}, @@ -58,7 +58,7 @@ "editor-indent-guides": {"assign": ["hediet"]}, "editor-input": {"assign": ["aiday-mar"]}, "editor-input-IME": {"assign": ["aiday-mar"]}, - "editor-insets": {"assign": ["jrieken"]}, + "editor-insets": {"assign": []}, "editor-minimap": {"assign": ["alexdima"]}, "editor-multicursor": {"assign": ["alexdima"]}, "editor-parameter-hints": {"assign": ["mjbvz"]}, @@ -68,7 +68,7 @@ "editor-scrollbar": {"assign": ["alexdima"]}, "editor-sorting": {"assign": ["alexdima"]}, "editor-sticky-scroll": {"assign": ["aiday-mar"]}, - "editor-symbols": {"assign": ["jrieken"]}, + "editor-symbols": {"assign": []}, "editor-synced-region": {"assign": ["aeschli"]}, "editor-textbuffer": {"assign": ["alexdima", "rebornix"]}, "editor-theming": {"assign": ["alexdima"]}, @@ -83,7 +83,7 @@ "extension-recommendations": {"assign": ["sandy081"]}, "extensions": {"assign": ["sandy081"]}, "extensions-development": {"assign": []}, - "file-decorations": {"assign": ["jrieken"]}, + "file-decorations": {"assign": []}, "file-encoding": {"assign": ["bpasero"]}, "file-explorer": {"assign": ["lramos15"]}, "file-glob": {"assign": ["bpasero"]}, @@ -91,7 +91,7 @@ "file-nesting": {"assign": ["lramos15"]}, "file-watcher": {"assign": ["bpasero"]}, "font-rendering": {"assign": ["rzhao271"]}, - "formatting": {"assign": ["jrieken"]}, + "formatting": {"assign": []}, "getting-started": {"assign": ["bhavyaus"]}, "ghost-text": {"assign": ["hediet"]}, "git": {"assign": ["lszomoru"]}, @@ -105,7 +105,7 @@ "icon-brand": {"assign": ["daviddossett"]}, "icons-product": {"assign": ["daviddossett"]}, "image-preview": {"assign": ["mjbvz"]}, - "inlay-hints": {"assign": ["jrieken", "hediet"]}, + "inlay-hints": {"assign": ["hediet"]}, "inline-completions": {"assign": ["hediet"]}, "install-update": {"assign": ["joaomoreno"], "accuracy": 0.85}, "intellisense-config": {"assign": ["rzhao271"]}, @@ -124,7 +124,7 @@ "l10n-platform": {"assign": ["TylerLeonhardt"]}, "label-provider": {"assign": ["lramos15"]}, "languages-basic": {"assign": ["aeschli"]}, - "languages-diagnostics": {"assign": ["jrieken"]}, + "languages-diagnostics": {"assign": []}, "languages-guessing": {"assign": ["TylerLeonhardt"]}, "layout": {"assign": ["benibenj"]}, "lcd-text-rendering": {"assign": []}, @@ -136,7 +136,7 @@ "menus": {"assign": ["sbatten"]}, "merge-conflict": {"assign": ["chrmarti"]}, "merge-editor": {"assign": ["hediet"]}, - "merge-editor-workbench": {"assign": ["jrieken"]}, + "merge-editor-workbench": {"assign": []}, "monaco-editor": {"assign": []}, "native-file-dialog": {"assign": ["deepak1556"]}, "network": {"assign": ["deepak1556"]}, @@ -179,7 +179,7 @@ "notebook-workflow": {"assign": []}, "open-editors": {"assign": ["lramos15"]}, "opener": {"assign": ["mjbvz"]}, - "outline": {"assign": ["jrieken"]}, + "outline": {"assign": []}, "output": {"assign": ["sandy081"]}, "perf": {"assign": []}, "perf-bloat": {"assign": []}, @@ -189,13 +189,13 @@ "proxy": {"assign": ["chrmarti"]}, "quick-open": {"assign": ["TylerLeonhardt"]}, "quick-pick": {"assign": ["TylerLeonhardt"]}, - "references-viewlet": {"assign": ["jrieken"]}, + "references-viewlet": {"assign": []}, "release-notes": {"assign": []}, "remote": {"assign": []}, "remote-connection": {"assign": ["alexdima"]}, "remote-explorer": {"assign": ["alexr00"]}, "remote-tunnel": {"assign": ["aeschli", "connor4312"]}, - "rename": {"assign": ["jrieken"]}, + "rename": {"assign": []}, "runCommands": {"assign": ["ulugbekna"]}, "sandbox": {"assign": ["deepak1556"]}, "sash-widget": {"assign": ["joaomoreno"]}, @@ -213,11 +213,11 @@ "settings-sync-server": {"assign": ["rzhao271"]}, "shared-process": {"assign": []}, "simple-file-dialog": {"assign": ["alexr00"]}, - "smart-select": {"assign": ["jrieken"]}, + "smart-select": {"assign": []}, "snap": {"assign": ["deepak1556"]}, - "snippets": {"assign": ["jrieken"]}, + "snippets": {"assign": []}, "splitview-widget": {"assign": ["joaomoreno"]}, - "suggest": {"assign": ["jrieken"]}, + "suggest": {"assign": []}, "table-widget": {"assign": ["joaomoreno"]}, "tasks": {"assign": ["meganrogge"], "accuracy": 0.85}, "telemetry": {"assign": ["lramos15"]}, @@ -263,11 +263,11 @@ "tree-sticky-scroll": {"assign": ["benibenj"]}, "tree-views": {"assign": ["alexr00"]}, "tree-widget": {"assign": ["joaomoreno"]}, - "typehierarchy": {"assign": ["jrieken"]}, + "typehierarchy": {"assign": []}, "typescript": {"assign": ["mjbvz"]}, "undo-redo": {"assign": ["alexdima"]}, "unicode-highlight": {"assign": ["hediet"]}, - "uri": {"assign": ["jrieken"]}, + "uri": {"assign": []}, "user-profiles": {"assign": ["sandy081"]}, "ux": {"assign": ["daviddossett"]}, "variable-resolving": {"assign": ["alexr00"]}, @@ -299,7 +299,7 @@ "workbench-multiroot": {"assign": ["bpasero"]}, "workbench-notifications": {"assign": ["bpasero"]}, "workbench-os-integration": {"assign": ["bpasero"]}, - "workbench-rapid-render": {"assign": ["jrieken"]}, + "workbench-rapid-render": {"assign": []}, "workbench-run-as-admin": {"assign": ["bpasero"]}, "workbench-state": {"assign": ["bpasero"]}, "workbench-status": {"assign": ["bpasero"]}, @@ -311,7 +311,7 @@ "workbench-window": {"assign": ["bpasero"]}, "workbench-workspace": {"assign": []}, "workbench-zen": {"assign": ["benibenj"]}, - "workspace-edit": {"assign": ["jrieken"]}, + "workspace-edit": {"assign": []}, "workspace-symbols": {"assign": []}, "workspace-trust": {"assign": ["lszomoru", "sbatten"]}, "zoom": {"assign": ["alexdima"] } From c3afbbfc6eaf7a48bec6304f2f3a147ce9f4899e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 24 Apr 2026 16:17:39 +0200 Subject: [PATCH 20/33] Add contributable Manage submenu to workspace picker (#312340) * Move remote hosts and connection actions under Manage submenu Add MenuId.SessionWorkspacePickerManage so providers can contribute actions to the workspace picker's Manage submenu. - Tunnels/SSH commands register into SessionWorkspacePickerManage menu - Remote provider entries shown as dynamic actions in the submenu - Remote providers and menu-contributed actions appear as separate visual groups with automatic separators - Remove now-unused inline remote provider items, status descriptions, and hover text from the main picker list Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add status, hover, and options to Manage submenu; move MenuId to sessions - Remote provider entries show status description (Online/Connecting/Offline) - Submenu items support hover panels via hoverContent property on actions - Clicking a remote provider opens host options (reconnect, remove, etc.) - Move SessionWorkspacePickerManage MenuId from platform actions.ts to sessions Menus.WorkspacePickerManage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address PR feedback: rename menu, use shortTitle, guard SSH on web - Rename WorkspacePickerManage to SessionWorkspaceManage - Use shortTitle for concise menu labels (SSH.../Tunnels...) while keeping descriptive title for Command Palette; read menu with renderShortTitle: true - Add IsWebContext.toNegated() precondition to SSH action so it is hidden in web contexts where SSH is not supported Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address feedback: use instanceof MenuItemAction, remove IsWebContext guard - Use instanceof MenuItemAction instead of 'item' in menuAction check - Remove IsWebContext precondition from SSH action Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../actionWidget/browser/actionList.ts | 3 +- src/vs/sessions/browser/menus.ts | 1 + .../chat/browser/scopedWorkspacePicker.ts | 6 + .../chat/browser/sessionWorkspacePicker.ts | 145 +++++++----------- .../browser/remoteAgentHostActions.ts | 14 ++ 5 files changed, 78 insertions(+), 91 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 84fc789f24967..e04bf3ebe14aa 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -1368,6 +1368,7 @@ export class ActionListWidget extends Disposable { const child = group.actions[ci]; const icon = (child as IAction & { icon?: ThemeIcon }).icon ?? ThemeIcon.fromId(child.checked ? Codicon.check.id : Codicon.blank.id); + const hoverContent = (child as IAction & { hoverContent?: string }).hoverContent; submenuItems.push({ item: child, kind: ActionListItemKind.Action, @@ -1375,7 +1376,7 @@ export class ActionListWidget extends Disposable { description: child.tooltip || undefined, group: { title: '', icon }, hideIcon: false, - hover: {}, + hover: hoverContent ? { content: hoverContent } : {}, }); } if (gi < groupsWithActions.length - 1) { diff --git a/src/vs/sessions/browser/menus.ts b/src/vs/sessions/browser/menus.ts index 9726373a2b977..c81d9ea0704eb 100644 --- a/src/vs/sessions/browser/menus.ts +++ b/src/vs/sessions/browser/menus.ts @@ -28,4 +28,5 @@ export const Menus = { NewSessionConfig: new MenuId('NewSessions.SessionConfigMenu'), NewSessionControl: new MenuId('NewSessions.SessionControlMenu'), NewSessionRepositoryConfig: new MenuId('NewSessions.RepositoryConfigMenu'), + SessionWorkspaceManage: new MenuId('Sessions.SessionWorkspaceManage'), } as const; diff --git a/src/vs/sessions/contrib/chat/browser/scopedWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/scopedWorkspacePicker.ts index 6285305cdca05..a1932e8538fce 100644 --- a/src/vs/sessions/contrib/chat/browser/scopedWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/scopedWorkspacePicker.ts @@ -7,10 +7,12 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { localize } from '../../../../nls.js'; import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; import { ActionListItemKind, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; +import { IMenuService } from '../../../../platform/actions/common/actions.js'; import { IRemoteAgentHostService } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -48,6 +50,8 @@ export class ScopedWorkspacePicker extends WorkspacePicker { @IConfigurationService configurationService: IConfigurationService, @ICommandService commandService: ICommandService, @IWorkspacesService workspacesService: IWorkspacesService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, @IAgentHostFilterService private readonly _agentHostFilterService: IAgentHostFilterService, ) { super( @@ -64,6 +68,8 @@ export class ScopedWorkspacePicker extends WorkspacePicker { configurationService, commandService, workspacesService, + menuService, + contextKeyService, ); // When the scoped host changes, if the current selection no longer diff --git a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts index 3db12e1538cae..92a348ef5d37b 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts @@ -8,19 +8,19 @@ import * as touch from '../../../../base/browser/touch.js'; import { IAction, SubmenuAction, toAction } from '../../../../base/common/actions.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { basename } from '../../../../base/common/resources.js'; -import { isNative } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; -import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus, RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; +import { IMenuService, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { TUNNEL_ADDRESS_PREFIX } from '../../../../platform/agentHost/common/tunnelAgentHost.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IPreferencesService } from '../../../../workbench/services/preferences/common/preferences.js'; import { IOutputService } from '../../../../workbench/services/output/common/output.js'; import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; @@ -35,6 +35,7 @@ import { ISessionsManagementService } from '../../../services/sessions/common/se import { IAgentHostSessionsProvider, isAgentHostProvider } from '../../../common/agentHostSessionsProvider.js'; import { COPILOT_PROVIDER_ID } from '../../copilotChatSessions/browser/copilotChatSessionsProvider.js'; import { IWorkspacesService, isRecentFolder } from '../../../../platform/workspaces/common/workspaces.js'; +import { Menus } from '../../../browser/menus.js'; const LEGACY_STORAGE_KEY_RECENT_PROJECTS = 'sessions.recentlyPickedProjects'; const STORAGE_KEY_RECENT_WORKSPACES = 'sessions.recentlyPickedWorkspaces'; @@ -66,8 +67,6 @@ export interface IWorkspacePickerItem { readonly selection?: IWorkspaceSelection; readonly browseActionIndex?: number; readonly checked?: boolean; - /** Remote provider reference for gear menu actions. */ - readonly remoteProvider?: IAgentHostSessionsProvider; /** Command to execute when this item is selected. */ readonly commandId?: string; } @@ -109,9 +108,11 @@ export class WorkspacePicker extends Disposable { @IClipboardService private readonly clipboardService: IClipboardService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IOutputService private readonly outputService: IOutputService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService _configurationService: IConfigurationService, @ICommandService private readonly commandService: ICommandService, @IWorkspacesService private readonly workspacesService: IWorkspacesService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); @@ -209,14 +210,7 @@ export class WorkspacePicker extends Disposable { // Workspace belongs to an unavailable remote — ignore selection return; } - if (item.remoteProvider && item.browseActionIndex === undefined) { - if (!item.remoteProvider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) { - // Disconnected SSH host — show options menu after widget hides. - // (Disconnected tunnels are rendered as disabled with a - // refresh toolbar action, so onSelect doesn't fire for them.) - this._showRemoteHostOptionsDelayed(item.remoteProvider); - } - } else if (item.browseActionIndex !== undefined) { + if (item.browseActionIndex !== undefined) { this._executeBrowseAction(item.browseActionIndex); } else if (item.selection) { this._selectProject(item.selection); @@ -453,107 +447,78 @@ export class WorkspacePicker extends Disposable { }); } - if (items.length > 0 && items[items.length - 1].kind !== ActionListItemKind.Separator && remoteProviders.length) { - items.push({ kind: ActionListItemKind.Separator, label: '' }); - } + // "Manage" submenu: dynamic remote provider entries + menu-contributed actions + // Dynamic remote provider entries + const remoteProviderActions: IAction[] = []; for (const provider of remoteProviders) { const status = provider.connectionStatus!.get(); - const isConnected = status === RemoteAgentHostConnectionStatus.Connected; - const providerBrowseIndex = allBrowseActions.findIndex(a => a.providerId === provider.id); const isTunnel = provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX); - - const toolbarActions: IAction[] = []; - - if (isTunnel) { - // Offline/connecting tunnels: surface a refresh button that - // attempts to (re)connect in case the cached status is stale. - if (!isConnected && providerBrowseIndex >= 0) { - const browseIndex = providerBrowseIndex; - toolbarActions.push(toAction({ - id: `workspacePicker.remote.refresh.${provider.id}`, - label: localize('workspacePicker.refreshTunnel', "Attempt to Connect"), - class: ThemeIcon.asClassName(Codicon.refresh), - run: () => { - this.actionWidgetService.hide(); - this._executeBrowseAction(browseIndex); - }, - })); - } - } else { - // Gear menu only for SSH hosts, not tunnel providers - toolbarActions.push(toAction({ - id: `workspacePicker.remote.gear.${provider.id}`, - label: localize('workspacePicker.remoteOptions', "Options"), - class: ThemeIcon.asClassName(Codicon.gear), - run: () => { - this.actionWidgetService.hide(); - this._showRemoteHostOptionsDelayed(provider); - }, - })); - } - - items.push({ - kind: ActionListItemKind.Action, + const action = toAction({ + id: `workspacePicker.remote.${provider.id}`, label: provider.label, - description: this._getStatusDescription(status), - hover: { content: this._getStatusHover(status, provider.remoteAddress) }, - group: { title: '', icon: isTunnel ? Codicon.cloud : Codicon.remote }, - disabled: !isConnected, - item: { - browseActionIndex: isConnected && providerBrowseIndex >= 0 ? providerBrowseIndex : undefined, - remoteProvider: provider, + tooltip: this._getStatusLabel(status), + enabled: true, + run: () => { + this.actionWidgetService.hide(); + this._showRemoteHostOptionsDelayed(provider); }, - toolbarActions, }); + const extended = action as IAction & { icon?: ThemeIcon; hoverContent?: string }; + extended.icon = isTunnel ? Codicon.cloud : Codicon.remote; + extended.hoverContent = this._getStatusHover(status, provider.remoteAddress); + remoteProviderActions.push(action); + } + + // Menu-contributed actions (e.g. Tunnels..., SSH...) + const menuContributedActions: IAction[] = []; + const menuActions = this.menuService.getMenuActions(Menus.SessionWorkspaceManage, this.contextKeyService, { renderShortTitle: true }); + for (const [, actions] of menuActions) { + for (const menuAction of actions) { + if (menuAction instanceof MenuItemAction) { + const icon = ThemeIcon.isThemeIcon(menuAction.item.icon) ? menuAction.item.icon : undefined; + menuContributedActions.push(Object.assign(menuAction, { icon })); + } + } } - // "Tunnels..." and "SSH..." entries — shown when remote agent hosts are enabled - if (this.configurationService.getValue(RemoteAgentHostsEnabledSettingId)) { + // Build submenu groups — each SubmenuAction becomes a visual group with + // automatic separators between them. + const manageSubmenuActions: SubmenuAction[] = []; + if (remoteProviderActions.length > 0) { + manageSubmenuActions.push(new SubmenuAction('workspacePicker.manage.remotes', '', remoteProviderActions)); + } + if (menuContributedActions.length > 0) { + manageSubmenuActions.push(new SubmenuAction('workspacePicker.manage.menu', '', menuContributedActions)); + } + + if (manageSubmenuActions.length > 0) { if (items.length > 0 && items[items.length - 1].kind !== ActionListItemKind.Separator) { items.push({ kind: ActionListItemKind.Separator, label: '' }); } items.push({ kind: ActionListItemKind.Action, - label: localize('workspacePicker.tunnels', "Tunnels..."), - group: { title: '', icon: Codicon.cloud }, - item: { commandId: 'workbench.action.sessions.connectViaTunnel' }, + label: localize('workspacePicker.manage', "Manage..."), + group: { title: '', icon: Codicon.settingsGear }, + item: {}, + submenuActions: manageSubmenuActions, }); - if (isNative) { - items.push({ - kind: ActionListItemKind.Action, - label: localize('workspacePicker.ssh', "SSH..."), - group: { title: '', icon: Codicon.remote }, - item: { commandId: 'workbench.action.sessions.connectViaSSH' }, - }); - } } return items; } - /** - * Returns a short status indicator with a colored circle icon for the description field. - */ - private _getStatusDescription(status: RemoteAgentHostConnectionStatus): MarkdownString { - const md = new MarkdownString(undefined, { supportThemeIcons: true }); + private _getStatusLabel(status: RemoteAgentHostConnectionStatus): string { switch (status) { case RemoteAgentHostConnectionStatus.Connected: - md.appendText(localize('workspacePicker.statusOnline', "Online")); - break; + return localize('workspacePicker.statusOnline', "Online"); case RemoteAgentHostConnectionStatus.Connecting: - md.appendText(localize('workspacePicker.statusConnecting', "Connecting")); - break; + return localize('workspacePicker.statusConnecting', "Connecting"); case RemoteAgentHostConnectionStatus.Disconnected: - md.appendText(localize('workspacePicker.statusOffline', "Offline")); - break; + return localize('workspacePicker.statusOffline', "Offline"); } - return md; } - /** - * Returns detailed hover text for a remote host's connection status. - */ private _getStatusHover(status: RemoteAgentHostConnectionStatus, address?: string): string { switch (status) { case RemoteAgentHostConnectionStatus.Connected: @@ -566,8 +531,8 @@ export class WorkspacePicker extends Disposable { : localize('workspacePicker.hoverConnecting', "Attempting to connect to remote agent host..."); case RemoteAgentHostConnectionStatus.Disconnected: return address - ? localize('workspacePicker.hoverDisconnectedAddr', "Remote agent host is disconnected. Click the gear icon for options.\n\nAddress: {0}", address) - : localize('workspacePicker.hoverDisconnected', "Remote agent host is disconnected. Click the gear icon for options."); + ? localize('workspacePicker.hoverDisconnectedAddr', "Remote agent host is disconnected.\n\nAddress: {0}", address) + : localize('workspacePicker.hoverDisconnected', "Remote agent host is disconnected."); } } diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts index 1ea5543af0345..64881379642de 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts @@ -5,6 +5,7 @@ import { localize, localize2 } from '../../../../nls.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { IRemoteAgentHostService, parseRemoteAgentHostInput, RemoteAgentHostEntryType, RemoteAgentHostInputValidationError, RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { ISSHRemoteAgentHostService, SSHAuthMethod, type ISSHAgentHostConfig, type ISSHAgentHostConnection, type ISSHResolvedConfig } from '../../../../platform/agentHost/common/sshRemoteAgentHost.js'; import { ITunnelAgentHostService, TUNNEL_ADDRESS_PREFIX, type ITunnelInfo } from '../../../../platform/agentHost/common/tunnelAgentHost.js'; @@ -16,6 +17,7 @@ import { IViewsService } from '../../../../workbench/services/views/common/views import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { SessionsCategories } from '../../../common/categories.js'; +import { Menus } from '../../../browser/menus.js'; import { NewChatViewPane, SessionsViewId } from '../../chat/browser/newChatViewPane.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js'; @@ -468,9 +470,15 @@ registerAction2(class extends Action2 { super({ id: 'workbench.action.sessions.connectViaSSH', title: localize2('connectViaSSH', "Connect to Remote Agent Host via SSH"), + shortTitle: localize2('connectViaSSHShort', "SSH..."), category: SessionsCategories.Sessions, f1: true, + icon: Codicon.remote, precondition: ContextKeyExpr.equals(`config.${RemoteAgentHostsEnabledSettingId}`, true), + menu: { + id: Menus.SessionWorkspaceManage, + order: 20, + }, }); } @@ -647,9 +655,15 @@ registerAction2(class extends Action2 { super({ id: 'workbench.action.sessions.connectViaTunnel', title: localize2('connectViaTunnel', "Connect to Remote Agent Host via Dev Tunnel"), + shortTitle: localize2('connectViaTunnelShort', "Tunnels..."), category: SessionsCategories.Sessions, f1: true, + icon: Codicon.cloud, precondition: ContextKeyExpr.equals(`config.${RemoteAgentHostsEnabledSettingId}`, true), + menu: { + id: Menus.SessionWorkspaceManage, + order: 10, + }, }); } From 9680ee18fe484347721bbe8cb4e5665950f2c1c9 Mon Sep 17 00:00:00 2001 From: winjo Date: Fri, 24 Apr 2026 22:18:01 +0800 Subject: [PATCH 21/33] Fix memory leak in AutoRepliesPtyServiceContribution on process dispose (#312150) `handleProcessDispose` only cleared the inner auto-responders map and never removed entries from `_terminalProcesses` or `_autoResponders`. Every exited pty stayed referenced, retaining the `TerminalProcess`, its `UnixTerminal`, the `_socket` (`ReadStream`), and the underlying pipe file descriptors / kernel buffers. Symptoms: JS heap barely grows between snapshots, but RSS climbs by tens of MiB per batch of spawned+exited terminals because the native backing resources are held alive. Fix: also `delete(persistentProcessId)` from both outer maps at the end of `handleProcessDispose`, so each map's size tracks the number of live ptys. --- .../terminalContrib/autoReplies/autoRepliesContribController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts b/src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts index bf75ded3d6f56..8cf4bd13b49b1 100644 --- a/src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts +++ b/src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts @@ -55,6 +55,8 @@ export class AutoRepliesPtyServiceContribution implements IPtyServiceContributio } processAutoResponders.clear(); } + this._autoResponders.delete(persistentProcessId); + this._terminalProcesses.delete(persistentProcessId); } handleProcessInput(persistentProcessId: number, data: string) { From 859b8c4e861634db24557a67f5ccb2a653876fab Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Fri, 24 Apr 2026 19:23:25 +0500 Subject: [PATCH 22/33] inline edit: fix spurious console warnings for reusedInFlight outcome transitions (#312347) fix: allow outcome transition from reusedInFlight in InlineEditRequestLogContext The 'reusedInFlight' outcome is set early when joining an in-flight request (before the result arrives), making it an intermediate state like 'pending'. Subsequent outcome updates (e.g., 'skipped' when the doc changes, 'errored' on failure) are legitimate transitions that should not trigger a console warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/platform/inlineEdits/common/inlineEditLogContext.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts b/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts index f518894dcd772..9f97b63dfd834 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts @@ -392,7 +392,10 @@ export class InlineEditRequestLogContext { * (e.g., in `setIsCachedResult` which intentionally overrides any inherited outcome). */ private _setOutcome(outcome: LogContextOutcome): void { - if (this._outcome !== 'pending') { + // 'reusedInFlight' is an intermediate state set when joining an in-flight + // request (before the result arrives), so it can legitimately transition + // to the final outcome (skipped, errored, etc.) just like 'pending'. + if (this._outcome !== 'pending' && this._outcome !== 'reusedInFlight') { console.warn(`[InlineEditRequestLogContext] outcome transition from '${this._outcome}' to '${outcome}' (request #${this.requestId})`); } this._outcome = outcome; From 29396e6fb4e3bf89302ea984d8f90e47ea7137fa Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 24 Apr 2026 15:53:33 +0100 Subject: [PATCH 23/33] Agents: Truncate chat input placeholder in narrow views (#312337) * chat: truncate chat input placeholder with ellipsis in narrow views Co-authored-by: Copilot * chat: improve placeholder truncation logic in narrow views Co-authored-by: Copilot * chat: enhance placeholder truncation logic for chat input in narrow views Co-authored-by: Copilot --------- Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../chat/browser/widget/media/chat.css | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 9864d359d5390..dc5a29920e3e8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1492,6 +1492,8 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-editor-container { padding: 0 0 0 4px; + /* enables 100cqi for placeholder truncation below */ + container-type: inline-size; } .interactive-session .interactive-input-part.compact .chat-editor-container { @@ -1503,6 +1505,23 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-input-foreground); } +/* Truncate the chat input placeholder with an ellipsis in narrow views + instead of letting it overflow off the right edge of the input. + The placeholder is rendered as a Monaco contentText decoration with + class prefix "ced-chat-session-detail" inside the view-line spans; + style the decoration element directly so we don't pay :has() cost + across every view-line on each typed character. */ +.chat-editor-container .monaco-editor [class^="ced-chat-session-detail"] { + display: inline-block; + max-width: 100%; /* fallback for environments without container query units */ + max-width: 100cqi; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; + color: var(--vscode-input-placeholderForeground); +} + .interactive-session .chat-editor-container .monaco-editor .chat-prompt-spinner { transform-origin: 6px 6px; font-size: 12px; From c5b38e3af0f4825b3dd43776400ee8ead58abc2e Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 24 Apr 2026 16:56:58 +0200 Subject: [PATCH 24/33] add ICustomizationHarnessService.getCustomAgentsorigin/aeschli (#312353) * add ICustomizationHarnessService.getCustomAgents * fix tests * update Co-authored-by: Copilot --------- Co-authored-by: Copilot --- .../copilotcli/node/copilotCli.ts | 2 + .../copilotCLICustomizationProvider.ts | 2 + .../remoteAgentHostCustomizationHarness.ts | 4 + .../api/browser/mainThreadChatAgents2.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostChatAgents2.ts | 4 +- .../aiCustomizationItemSource.ts | 6 + ...promptsServiceCustomizationItemProvider.ts | 20 ++- .../contrib/chat/common/chatModes.ts | 11 +- .../common/customizationHarnessService.ts | 74 +++++++++- .../promptSyntax/service/promptsService.ts | 1 - .../service/promptsServiceImpl.ts | 132 ++++++++++-------- .../customizationHarnessService.test.ts | 96 ++++++++++++- .../service/mockPromptsService.ts | 2 +- .../service/promptsService.test.ts | 15 +- 15 files changed, 281 insertions(+), 91 deletions(-) diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts index 6898c09c7b387..1805638b146a6 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts @@ -278,6 +278,8 @@ export interface CLIAgentInfo { readonly agent: Readonly; /** File URI for prompt-file agents, synthetic `copilotcli:` URI for SDK-only agents. */ readonly sourceUri: URI; + readonly extensionId?: string; + readonly pluginUri?: URI; } export interface ICopilotCLIAgents { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts index d0af1a6f79dc7..95ff49d8534e5 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts @@ -189,6 +189,8 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod uri: s.uri, type: vscode.ChatSessionCustomizationType.Skill, name: s.name, + extensionId: s.extensionId, + pluginUri: s.pluginUri, })); } diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts index 58ef0bf07a00b..a6ef3df422be7 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts @@ -90,6 +90,8 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements status: toStatusString(sc.status), statusMessage: sc.statusMessage, enabled: sc.enabled, + extensionId: undefined, + pluginUri: undefined })); } @@ -99,6 +101,8 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements type: 'plugin', name: ref.displayName, description: ref.description, + extensionId: undefined, + pluginUri: undefined })); } } diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index fed4a81015adb..db1ab19404444 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -746,6 +746,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA groupKey: item.groupKey, badge: item.badge, badgeTooltip: item.badgeTooltip, + extensionId: undefined, + pluginUri: undefined })); }, }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f03878df02b2a..2ba3793dc283a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1738,6 +1738,7 @@ export interface IChatSessionCustomizationItemDto { readonly description?: string; readonly groupKey?: string; readonly badge?: string; + readonly badgeTooltip?: string; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 58b98ea5a42df..0b58d688ac7e4 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -830,8 +830,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS description: item.description, groupKey: item.groupKey, badge: item.badge, - badgeTooltip: item.badgeTooltip, - })); + badgeTooltip: item.badgeTooltip + } satisfies IChatSessionCustomizationItemDto)); } catch (err) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts index f394aa0176355..2d643c9065853 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts @@ -156,6 +156,8 @@ export async function expandHookFileItems( enabled: item.enabled, groupKey: item.groupKey, storage: item.storage, + extensionId: item.extensionId, + pluginUri: item.pluginUri }); } } @@ -449,6 +451,8 @@ export class ProviderCustomizationItemSource implements IAICustomizationItemSour enabled: !disabledPromptFiles.has(p.uri), badge: uiTooltip ? uiIntegrationBadge : undefined, badgeTooltip: uiTooltip, + extensionId: undefined, + pluginUri: undefined }; appended.push(this.itemNormalizer.normalizeItem(builtinItem, promptType, uriUseCounts)); } @@ -484,6 +488,8 @@ export class ProviderCustomizationItemSource implements IAICustomizationItemSour name: getFriendlyName(basename(file.uri)), groupKey: 'sync-local', enabled: true, + extensionId: undefined, + pluginUri: undefined })); return this.itemNormalizer.normalizeItems(providerItems, promptType) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts index 7e710a7c5232b..7f8c78a4efe9b 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts @@ -75,6 +75,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt description: agent.description, storage: agent.source.storage, enabled: !disabledUris.has(agent.uri), + extensionId: agent.source.storage === PromptsStorage.extension ? agent.source.extensionId.value : undefined, + pluginUri: agent.source.storage === PromptsStorage.plugin ? agent.source.pluginUri : undefined }); if (agent.source.storage === PromptsStorage.extension && !extensionInfoByUri.has(agent.uri)) { extensionInfoByUri.set(agent.uri, { id: agent.source.extensionId }); @@ -104,6 +106,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt enabled: true, badge: uiTooltip ? localize('uiIntegrationBadge', "UI Integration") : undefined, badgeTooltip: uiTooltip, + extensionId: skill.extension?.identifier.value, + pluginUri: skill.pluginUri }); } if (disabledUris.size > 0) { @@ -121,6 +125,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt enabled: false, badge: uiTooltip ? localize('uiIntegrationBadge', "UI Integration") : undefined, badgeTooltip: uiTooltip, + extensionId: file.extension?.identifier.value, + pluginUri: file.pluginUri }); } } @@ -138,6 +144,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt description: command.description, storage: command.storage, enabled: !disabledUris.has(command.uri), + extensionId: command.extension?.identifier.value, + pluginUri: command.pluginUri }); if (command.extension) { extensionInfoByUri.set(command.uri, { id: command.extension.identifier, displayName: command.extension.displayName }); @@ -166,6 +174,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt name: f.name || getFriendlyName(basename(f.uri)), storage: f.storage, enabled: !disabledUris.has(f.uri), + extensionId: f.extension?.identifier.value, + pluginUri: f.pluginUri }); } @@ -193,6 +203,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt storage: agent.source.storage, groupKey: 'agents', enabled: !disabledUris.has(agent.uri), + extensionId: agent.source.storage === PromptsStorage.extension ? agent.source.extensionId.value : undefined, + pluginUri: agent.source.storage === PromptsStorage.plugin ? agent.source.pluginUri : undefined }); } } @@ -219,10 +231,12 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt storage, groupKey: 'agent-instructions', enabled: !disabledUris.has(file.uri), + extensionId: undefined, + pluginUri: undefined }); } - for (const { uri, pattern, name, description, storage } of instructionFiles) { + for (const { uri, pattern, name, description, storage, extension, pluginUri } of instructionFiles) { if (agentInstructionUris.has(uri)) { continue; } @@ -246,6 +260,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt storage, groupKey: 'context-instructions', enabled: !disabledUris.has(uri), + extensionId: extension?.identifier.value, + pluginUri }); } else { items.push({ @@ -256,6 +272,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt storage, groupKey: 'on-demand-instructions', enabled: !disabledUris.has(uri), + extensionId: extension?.identifier.value, + pluginUri }); } } diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index ce81c152d2095..b510a3e306b12 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -494,7 +494,7 @@ function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSou return undefined; } if (source.storage === PromptsStorage.extension) { - return { storage: PromptsStorage.extension, extensionId: source.extensionId.value, type: source.type }; + return { storage: PromptsStorage.extension, extensionId: source.extensionId.value }; } if (source.storage === PromptsStorage.plugin) { return { storage: PromptsStorage.plugin, pluginUri: source.pluginUri }; @@ -507,14 +507,7 @@ function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSour return undefined; } if (data.storage === PromptsStorage.extension) { - // Migrate old ExtensionAgentSourceType values ('contribution'/'provider') to PromptFileSource values - let type: PromptFileSource.ExtensionContribution | PromptFileSource.ExtensionAPI; - if (data.type === 'provider' as string /* old type value */ || data.type === PromptFileSource.ExtensionAPI) { - type = PromptFileSource.ExtensionAPI; - } else { - type = PromptFileSource.ExtensionContribution; - } - return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId), type }; + return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId) }; } if (data.storage === PromptsStorage.plugin) { return { storage: PromptsStorage.plugin, pluginUri: URI.revive(data.pluginUri) }; diff --git a/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts b/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts index a7fa2f855fffb..83b2a474a7b57 100644 --- a/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts +++ b/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts @@ -15,9 +15,11 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { AICustomizationManagementSection, IStorageSourceFilter } from './aiCustomizationWorkspaceService.js'; import { PromptsType } from './promptSyntax/promptTypes.js'; import { AGENT_MD_FILENAME } from './promptSyntax/config/promptFileLocations.js'; -import { IChatPromptSlashCommand, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js'; +import { IAgentSource, IChatPromptSlashCommand, ICustomAgent, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { SessionType } from './chatSessionsService.js'; +import { CustomAgent } from './promptSyntax/service/promptsServiceImpl.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; export const ICustomizationHarnessService = createDecorator('customizationHarnessService'); @@ -146,6 +148,10 @@ export interface ICustomizationItem { readonly storage?: PromptsStorage; /** Display name of the contributing extension (e.g. "GitHub Copilot Chat"). */ readonly extensionLabel?: string; + /** The extension identifier that contributed this customization, if any. */ + readonly extensionId: string | undefined; + /** The URI of the plugin that contributed this customization, if any. */ + readonly pluginUri: URI | undefined; /** Server-reported loading status for this customization. */ readonly status?: 'loading' | 'loaded' | 'degraded' | 'error'; /** Human-readable status detail (e.g. error message or warning). */ @@ -265,6 +271,11 @@ export interface ICustomizationHarnessService { */ readonly onDidChangeSlashCommands: Event<{ readonly sessionType: string }>; + /** + * Fires when one of the provided custom agents changes. + */ + readonly onDidChangeCustomAgents: Event<{ readonly sessionType: string }>; + /** * Returns the prompt and skill slash commands for the given session type. * Provider-backed harnesses contribute their own items directly; the default @@ -272,6 +283,13 @@ export interface ICustomizationHarnessService { */ getSlashCommands(sessionType: string, token: CancellationToken): Promise; + /** + * Returns the custom agents for the given session type. + * Provider-backed harnesses select items via their own provider and resolve + * details via the core prompts service. + */ + getCustomAgents(sessionType: string, token: CancellationToken): Promise; + /** * Resolves a slash command to its full metadata, including the parsed prompt file for prompt commands. * Provider-backed harnesses resolve their own items directly; the default VS Code harness falls back to the core prompts service. @@ -481,7 +499,10 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer declare readonly _serviceBrand: undefined; private readonly _onDidChangeSlashCommands = new Emitter<{ readonly sessionType: string }>(); readonly onDidChangeSlashCommands = this._onDidChangeSlashCommands.event; + private readonly _onDidChangeCustomAgents = new Emitter<{ readonly sessionType: string }>(); + readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event; private readonly _providerListeners: IDisposable[] = []; + private _isDisposed = false; private readonly _activeHarness: ISettableObservable; readonly activeHarness: IObservable; @@ -516,6 +537,9 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer } private _refreshAvailableHarnesses(): void { + if (this._isDisposed) { + return; + } this._availableHarnesses.set(this._getAllHarnesses(), undefined); this._rebindProviderListeners(); } @@ -529,18 +553,22 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer const provider = harness.itemProvider; if (!provider) { this._providerListeners.push(this.promptsService.onDidChangeSlashCommands(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id }))); + this._providerListeners.push(this.promptsService.onDidChangeCustomAgents(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id }))); } else { this._providerListeners.push(provider.onDidChange(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id }))); + this._providerListeners.push(provider.onDidChange(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id }))); } } } dispose(): void { + this._isDisposed = true; for (const listener of this._providerListeners) { listener.dispose(); } this._providerListeners.length = 0; this._onDidChangeSlashCommands.dispose(); + this._onDidChangeCustomAgents.dispose(); } registerExternalHarness(descriptor: IHarnessDescriptor): IDisposable { @@ -548,6 +576,9 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer this._refreshAvailableHarnesses(); return { dispose: () => { + if (this._isDisposed) { + return; + } const idx = this._externalHarnesses.indexOf(descriptor); if (idx >= 0) { this._externalHarnesses.splice(idx, 1); @@ -624,6 +655,47 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer return result; } + async getCustomAgents(sessionType: string, token: CancellationToken): Promise { + const harness = this.findHarnessById(sessionType); + if (!harness || !harness.itemProvider) { + const allAgents = await this.promptsService.getCustomAgents(token); + return allAgents.filter(agent => matchesSessionType(agent.sessionTypes, sessionType)); + } + + const items = await harness.itemProvider.provideChatSessionCustomizations(token); + if (!items) { + return []; + } + + const getSource = (item: ICustomizationItem): IAgentSource => { + if (item.storage === PromptsStorage.extension && item.extensionId) { + return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(item.extensionId) }; + } else if (item.storage === PromptsStorage.plugin && item.pluginUri) { + return { storage: PromptsStorage.plugin, pluginUri: item.pluginUri }; + } else if (item.storage === PromptsStorage.user) { + return { storage: PromptsStorage.user }; + } + return { storage: PromptsStorage.local }; + }; + + const result: ICustomAgent[] = []; + for (const item of items) { + if ((item.enabled !== false) && item.type === PromptsType.agent) { + const promptFile = await this.promptsService.parseNew(item.uri, token); + const extra = { + name: item.name, + description: item.description, + sessionTypes: [sessionType], + hooks: undefined, + source: getSource(item), + type: PromptsType.agent, + }; + result.push(CustomAgent.fromParsedPromptFile(promptFile, extra)); + } + } + return result; + } + public async resolvePromptSlashCommand(name: string, sessionType: string, token: CancellationToken): Promise { const harness = this.findHarnessById(sessionType); if (!harness || !harness.itemProvider) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 70735295b98fd..17fd0c835e7eb 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -196,7 +196,6 @@ export interface IPluginPromptPath extends IPromptPathBase { export type IAgentSource = { readonly storage: PromptsStorage.extension; readonly extensionId: ExtensionIdentifier; - readonly type: PromptFileSource.ExtensionContribution | PromptFileSource.ExtensionAPI; } | { readonly storage: PromptsStorage.local | PromptsStorage.user; } | { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index ef662ec845fb7..74de7de710220 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -791,73 +791,24 @@ export class PromptsService extends Disposable implements IPromptsService { try { const ast = await this.parseNew(uri, token); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let metadata: any | undefined; - if (ast.header) { - const advanced = ast.header.getAttribute(PromptHeaderAttributes.advancedOptions); - if (advanced && advanced.value.type === 'map') { - metadata = {}; - for (const [key, value] of Object.entries(advanced.value)) { - if (value.type === 'scalar') { - metadata[key] = value; - } - } - } - } - const toolReferences: IVariableReference[] = []; - if (ast.body) { - const bodyOffset = ast.body.offset; - const bodyVarRefs = ast.body.variableReferences; - for (let i = bodyVarRefs.length - 1; i >= 0; i--) { // in reverse order - const { name, offset, fullLength } = bodyVarRefs[i]; - const range = new OffsetRange(offset - bodyOffset, offset - bodyOffset + fullLength); - toolReferences.push({ name, range }); - } - } - - const agentInstructions = { - content: ast.body?.getContent() ?? '', - toolReferences, - metadata, - } satisfies IAgentInstructions; - - const name = ast.header?.name ?? promptPath.name ?? getCleanPromptName(uri); - const description = ast.header?.description ?? promptPath.description; - const target = getTarget(PromptsType.agent, ast.header ?? uri); - - const source: IAgentSource = IAgentSource.fromPromptPath(promptPath); - const when = isExtensionPromptPath(promptPath) && promptPath.when - ? ContextKeyExpr.deserialize(promptPath.when) ?? undefined - : undefined; - if (!ast.header) { - const agent: ICustomAgent = { uri, name, agentInstructions, source, target, visibility: { userInvocable: true, agentInvocable: true }, sessionTypes: promptPath.sessionTypes, ...(when !== undefined ? { when } : undefined) }; - return { status: 'loaded', promptPath: this.withPromptPathMetadata(promptPath, name, description), agent }; - } - const visibility = { - userInvocable: ast.header.userInvocable !== false, - agentInvocable: ast.header.infer !== undefined ? ast.header.infer === true : ast.header.disableModelInvocation !== true, - } satisfies ICustomAgentVisibility; - - let model = ast.header.model; - if (target === Target.Claude && model) { - model = mapClaudeModels(model); - } - let { tools, handOffs, argumentHint, agents } = ast.header; - if (target === Target.Claude && tools) { - tools = mapClaudeTools(tools); - } - // Parse hooks from the frontmatter if present let hooks: ChatRequestHooks | undefined; - const hooksRaw = ast.header.hooksRaw; + const hooksRaw = ast.header?.hooksRaw; if (useChatHooks && isWorkspaceTrusted && hooksRaw) { const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(uri) ?? defaultFolder; const workspaceRootUri = hookWorkspaceFolder?.uri; + const target = getTarget(PromptsType.agent, ast.header ?? promptPath.uri); hooks = parseSubagentHooksFromYaml(hooksRaw, workspaceRootUri, userHome, target); } - - const agent: ICustomAgent = { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, hooks, agentInstructions, source, sessionTypes: promptPath.sessionTypes, ...(when !== undefined ? { when } : undefined) }; - return { status: 'loaded', promptPath: this.withPromptPathMetadata(promptPath, name, description), agent }; + const extra = { + sessionTypes: promptPath.sessionTypes, + hooks, + name: promptPath.name, + description: promptPath.description, + source: IAgentSource.fromPromptPath(promptPath) + }; + const agent = CustomAgent.fromParsedPromptFile(ast, extra); + return { status: 'loaded', promptPath: this.withPromptPathMetadata(promptPath, agent.name, agent.description), agent }; } catch (e) { const error = e instanceof Error ? e : new Error(String(e)); if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { @@ -1788,13 +1739,70 @@ class ModelChangeTracker extends Disposable { } } +export namespace CustomAgent { + export function fromParsedPromptFile(ast: ParsedPromptFile, extra: { name?: string; description?: string; when?: string; source: IAgentSource; hooks?: ChatRequestHooks; sessionTypes: readonly string[] | undefined }): ICustomAgent { + const uri = ast.uri; + const { hooks, sessionTypes } = extra; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let metadata: any | undefined; + if (ast.header) { + const advanced = ast.header.getAttribute(PromptHeaderAttributes.advancedOptions); + if (advanced && advanced.value.type === 'map') { + metadata = {}; + for (const [key, value] of Object.entries(advanced.value)) { + if (value.type === 'scalar') { + metadata[key] = value; + } + } + } + } + const toolReferences: IVariableReference[] = []; + if (ast.body) { + const bodyOffset = ast.body.offset; + const bodyVarRefs = ast.body.variableReferences; + for (let i = bodyVarRefs.length - 1; i >= 0; i--) { // in reverse order + const { name, offset, fullLength } = bodyVarRefs[i]; + const range = new OffsetRange(offset - bodyOffset, offset - bodyOffset + fullLength); + toolReferences.push({ name, range }); + } + } + + const agentInstructions = { content: ast.body?.getContent() ?? '', toolReferences, metadata } satisfies IAgentInstructions; + + const name = ast.header?.name ?? extra.name ?? getCleanPromptName(uri); + const description = ast.header?.description ?? extra.description; + const target = getTarget(PromptsType.agent, ast.header ?? uri); + + const when = extra.when ? ContextKeyExpr.deserialize(extra.when) ?? undefined : undefined; + const source = extra.source; + if (!ast.header) { + return { uri, name, agentInstructions, source, target, visibility: { userInvocable: true, agentInvocable: true }, sessionTypes, hooks, when }; + } + const visibility = { + userInvocable: ast.header.userInvocable !== false, + agentInvocable: ast.header.infer !== undefined ? ast.header.infer === true : ast.header.disableModelInvocation !== true, + } satisfies ICustomAgentVisibility; + + let model = ast.header.model; + if (target === Target.Claude && model) { + model = mapClaudeModels(model); + } + let { tools, handOffs, argumentHint, agents } = ast.header; + if (target === Target.Claude && tools) { + tools = mapClaudeTools(tools); + } + return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, agentInstructions, source, sessionTypes, hooks, when }; + + } +} + namespace IAgentSource { export function fromPromptPath(promptPath: IPromptPath): IAgentSource { if (promptPath.storage === PromptsStorage.extension) { return { storage: PromptsStorage.extension, - extensionId: promptPath.extension.identifier, - type: promptPath.source + extensionId: promptPath.extension.identifier }; } else if (promptPath.storage === PromptsStorage.plugin) { return { diff --git a/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts b/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts index 4eb4cdffc88f3..d0a384e9aec7e 100644 --- a/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts @@ -9,8 +9,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { CustomizationHarnessServiceBase, createVSCodeHarnessDescriptor, ICustomizationItemProvider, IHarnessDescriptor, matchesWorkspaceSubpath } from '../../common/customizationHarnessService.js'; -import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; -import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { PromptsType, Target } from '../../common/promptSyntax/promptTypes.js'; +import { ICustomAgent, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { SessionType } from '../../common/chatSessionsService.js'; import { MockPromptsService } from './promptSyntax/service/mockPromptsService.js'; @@ -55,6 +55,32 @@ suite('CustomizationHarnessService', () => { assert.strictEqual(firedSessionType, harnessId); }); + test('forwards item provider changes via onDidChangeCustomAgents with sessionType', () => { + const service = createService(); + const emitter = new Emitter(); + store.add(emitter); + const harnessId = 'test-harness'; + const externalDescriptor: IHarnessDescriptor = { + id: harnessId, + label: 'Test Harness', + icon: ThemeIcon.fromId('extensions'), + getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }), + itemProvider: { + onDidChange: emitter.event, + provideChatSessionCustomizations: async () => [], + }, + }; + + store.add(service.registerExternalHarness(externalDescriptor)); + + let firedSessionType: string | undefined; + const listener = store.add(service.onDidChangeCustomAgents(e => firedSessionType = e.sessionType)); + store.add(listener); + + emitter.fire(); + assert.strictEqual(firedSessionType, harnessId); + }); + test('adds harness to available list', () => { const service = createService(); assert.strictEqual(service.availableHarnesses.get().length, 1); @@ -175,7 +201,7 @@ suite('CustomizationHarnessService', () => { const emitter = new Emitter(); store.add(emitter); const testItems = [ - { uri: URI.parse('file:///workspace/.claude/SKILL.md'), type: 'skill', name: 'Test Skill', description: 'A test skill' }, + { uri: URI.parse('file:///workspace/.claude/SKILL.md'), type: 'skill', name: 'Test Skill', description: 'A test skill', extensionId: undefined, pluginUri: undefined }, ]; const itemProvider: ICustomizationItemProvider = { @@ -346,10 +372,10 @@ suite('CustomizationHarnessService', () => { itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async () => [ - { uri: URI.parse('file:///workspace/.test/prompts/fix.prompt.md'), type: PromptsType.prompt, name: 'fix', description: 'Fix something' }, - { uri: URI.parse('file:///workspace/.test/skills/lint/SKILL.md'), type: PromptsType.skill, name: 'lint', description: 'Lint skill' }, - { uri: URI.parse('file:///workspace/.test/instructions/rule.instructions.md'), type: PromptsType.instructions, name: 'rule', description: 'Ignore me' }, - { uri: URI.parse('file:///workspace/.test/skills/disabled/SKILL.md'), type: PromptsType.skill, name: 'disabled', enabled: false }, + { uri: URI.parse('file:///workspace/.test/prompts/fix.prompt.md'), type: PromptsType.prompt, name: 'fix', description: 'Fix something', extensionId: undefined, pluginUri: undefined }, + { uri: URI.parse('file:///workspace/.test/skills/lint/SKILL.md'), type: PromptsType.skill, name: 'lint', description: 'Lint skill', extensionId: undefined, pluginUri: undefined }, + { uri: URI.parse('file:///workspace/.test/instructions/rule.instructions.md'), type: PromptsType.instructions, name: 'rule', description: 'Ignore me', extensionId: undefined, pluginUri: undefined }, + { uri: URI.parse('file:///workspace/.test/skills/disabled/SKILL.md'), type: PromptsType.skill, name: 'disabled', enabled: false, extensionId: undefined, pluginUri: undefined }, ], }, }); @@ -391,6 +417,62 @@ suite('CustomizationHarnessService', () => { }); }); + suite('getCustomAgents', () => { + const createAgent = (name: string, path: string, sessionTypes?: readonly string[]): ICustomAgent => ({ + uri: URI.parse(path), + name, + target: Target.GitHubCopilot, + visibility: { userInvocable: true, agentInvocable: true }, + agentInstructions: { content: '', toolReferences: [] }, + source: { storage: PromptsStorage.local }, + sessionTypes, + }); + + test('falls back to promptsService and filters by session type', async () => { + const testSessionType = 'test-session-type'; + const promptsService = new MockPromptsService(); + promptsService.setCustomModes([ + createAgent('matching', 'file:///workspace/.github/agents/matching.agent.md', [testSessionType]), + createAgent('global', 'file:///workspace/.github/agents/global.agent.md'), + createAgent('other', 'file:///workspace/.github/agents/other.agent.md', ['other-session']), + ]); + const service = new CustomizationHarnessServiceBase([createVSCodeHarnessDescriptor([PromptsStorage.extension])], SessionType.Local, promptsService); + store.add(service); + + const agents = await service.getCustomAgents(testSessionType, CancellationToken.None); + assert.deepStrictEqual(agents.map(agent => agent.name), ['matching', 'global']); + }); + + test('uses provider item URIs to scope resolved custom agents', async () => { + const testSessionType = 'test-session-type'; + const promptsService = new MockPromptsService(); + promptsService.setCustomModes([ + createAgent('selected', 'file:///workspace/.test/agents/selected.agent.md', [testSessionType]), + createAgent('not-selected', 'file:///workspace/.test/agents/not-selected.agent.md', [testSessionType]), + ]); + + const emitter = new Emitter(); + store.add(emitter); + const service = new CustomizationHarnessServiceBase([{ + id: testSessionType, + label: 'Test Extension', + icon: ThemeIcon.fromId('extensions'), + getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }), + itemProvider: { + onDidChange: emitter.event, + provideChatSessionCustomizations: async () => [ + { uri: URI.parse('file:///workspace/.test/agents/selected.agent.md'), type: PromptsType.agent, name: 'selected', extensionId: undefined, pluginUri: undefined }, + { uri: URI.parse('file:///workspace/.test/agents/disabled.agent.md'), type: PromptsType.agent, name: 'disabled', enabled: false, extensionId: undefined, pluginUri: undefined }, + ], + }, + }], testSessionType, promptsService); + store.add(service); + + const agents = await service.getCustomAgents(testSessionType, CancellationToken.None); + assert.deepStrictEqual(agents.map(agent => agent.name), ['selected']); + }); + }); + suite('matchesWorkspaceSubpath', () => { test('matches segment boundary', () => { assert.ok(matchesWorkspaceSubpath('/workspace/.claude/skills/SKILL.md', ['.claude'])); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts index c4e34dcec8916..47828f1ad080e 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts @@ -51,7 +51,7 @@ export class MockPromptsService implements IPromptsService { // eslint-disable-next-line @typescript-eslint/no-explicit-any parse(_uri: URI, _type: any, _token: CancellationToken): Promise { throw new Error('Not implemented'); } // eslint-disable-next-line @typescript-eslint/no-explicit-any - parseNew(_uri: URI, _token: CancellationToken): Promise { throw new Error('Not implemented'); } + parseNew(uri: URI, _token: CancellationToken): Promise { return Promise.resolve({ uri }); } getParsedPromptFile(textModel: ITextModel): ParsedPromptFile { throw new Error('Not implemented'); } registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined, when?: string, sessionTypes?: readonly string[]): IDisposable { throw new Error('Not implemented'); } getPromptLocationLabel(promptPath: IPromptPath): string { throw new Error('Not implemented'); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 743114b8eaf5e..fc58f19982daf 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -811,7 +811,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'agent1', @@ -869,7 +869,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'agent1', @@ -901,6 +901,7 @@ suite('PromptsService', () => { ], metadata: undefined }, + hooks: undefined, sessionTypes: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local }, @@ -947,7 +948,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'agent1', @@ -1039,7 +1040,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'github-agent', @@ -1157,7 +1158,7 @@ suite('PromptsService', () => { }, ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'copilot-agent', @@ -1259,7 +1260,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'demonstrate', @@ -1331,7 +1332,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'restricted-agent', From 904a60ee4f166a17f4daa544dfc948bc516a30d9 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Fri, 24 Apr 2026 16:39:55 +0200 Subject: [PATCH 25/33] nes: lang ctx: register for NES as well gated with NES setting for including lang ctx Co-authored-by: Copilot --- .../typescriptContext/vscode-node/languageContextService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts b/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts index 7f48633ba47c0..fdb2488c7d9d5 100644 --- a/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts +++ b/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts @@ -14,13 +14,13 @@ import { ILogService } from '../../../platform/log/common/logService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { Queue } from '../../../util/vs/base/common/async'; +import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { DisposableStore } from '../../../util/vs/base/common/lifecycle'; import { generateUuid } from '../../../util/vs/base/common/uuid'; import * as protocol from '../common/serverProtocol'; import { InspectorDataProvider } from './inspector'; import { ThrottledDebouncer } from './throttledDebounce'; import { ContextItemResultBuilder, ContextItemSummary, ResolvedRunnableResult, type OnCachePopulatedEvent, type OnContextComputedEvent, type OnContextComputedOnTimeoutEvent } from './types'; -import { CancellationToken } from '../../../util/vs/base/common/cancellation'; const currentTokenBudget: number = 8 * 1024; @@ -1975,7 +1975,7 @@ export class InlineCompletionContribution implements vscode.Disposable, TokenBud } // Register with chat always. - this.registrations.add(this.languageContextProviderService.registerContextProvider(provider, [ProviderTarget.Completions])); + this.registrations.add(this.languageContextProviderService.registerContextProvider(provider, [ProviderTarget.Completions, ProviderTarget.NES])); this.telemetrySender.sendInlineCompletionProviderTelemetry(KnownSources.completion, true); logService.info('Registered TypeScript context provider with Copilot inline completions.'); } catch (error) { From cae7455f0d3add2081837231f62edcca81cd0f83 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 24 Apr 2026 17:10:30 +0200 Subject: [PATCH 26/33] sessions: restore agents application support on Linux (#312360) Restore agents application support on Linux Remove the Linux platform check that was added in 683373f3, re-enabling the agents window on Linux for non-stable builds. - Restore `ProductQualityContext.notEqualsTo('stable')` in the command precondition instead of the Linux-specific context key expression - Clean up unused `isLinux`/`IsLinuxContext` imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/code/electron-main/app.ts | 2 +- src/vs/platform/launch/electron-main/launchMainService.ts | 4 ++-- src/vs/workbench/contrib/chat/common/constants.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index b2af5e37e80a0..bf4043983fdb1 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -1397,7 +1397,7 @@ export class CodeApplication extends Disposable { } // Handle agents window first based on context - if ((process as INodeProcess).isEmbeddedApp || (!isLinux && args['agents'] && this.productService.quality !== 'stable')) { + if ((process as INodeProcess).isEmbeddedApp || (args['agents'] && this.productService.quality !== 'stable')) { return windowsMainService.openAgentsWindow({ context, cli: args, diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 1faa200f8ce2f..7815fdff84402 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -6,7 +6,7 @@ import { app } from 'electron'; import { coalesce } from '../../../base/common/arrays.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { IProcessEnvironment, isLinux, isMacintosh } from '../../../base/common/platform.js'; +import { IProcessEnvironment, isMacintosh } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { whenDeleted } from '../../../base/node/pfs.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -164,7 +164,7 @@ export class LaunchMainService implements ILaunchMainService { } // Agents window - else if (!isLinux && args['agents'] && this.productService.quality !== 'stable') { + else if (args['agents'] && this.productService.quality !== 'stable') { usedWindows = await this.windowsMainService.openAgentsWindow(baseConfig); } diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index d8b405fdd712c..613f5cfeaeee7 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -7,7 +7,7 @@ import { Schemas } from '../../../../base/common/network.js'; import { IChatSessionsService } from './chatSessionsService.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IsDevelopmentContext, IsLinuxContext } from '../../../../platform/contextkey/common/contextkeys.js'; +import { ProductQualityContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { ChatEntitlementContextKeys } from '../../../services/chat/common/chatEntitlementService.js'; import { IsSessionsWindowContext } from '../../../common/contextkeys.js'; @@ -198,7 +198,7 @@ export const MANAGE_CHAT_COMMAND_ID = 'workbench.action.chat.manage'; export const OPEN_AGENTS_WINDOW_COMMAND_ID = 'workbench.action.openAgentsWindow'; export const OPEN_AGENTS_WINDOW_PRECONDITION = ContextKeyExpr.and( - ContextKeyExpr.or(IsLinuxContext.negate(), IsDevelopmentContext), + ProductQualityContext.notEqualsTo('stable'), ChatEntitlementContextKeys.Setup.hidden.negate(), ChatEntitlementContextKeys.Setup.disabledInWorkspace.negate(), IsSessionsWindowContext.negate(), From c01e67a07a0bcaebd2e29d49fa6067437bda24c2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:33:45 +0000 Subject: [PATCH 27/33] Engineering - increase timeout for the web stage (#312355) --- build/azure-pipelines/web/product-build-web.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 118d9d2b60f69..8df553bc2b79d 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -1,7 +1,7 @@ jobs: - job: Web displayName: Web - timeoutInMinutes: 30 + timeoutInMinutes: 45 pool: name: 1es-ubuntu-22.04-x64 os: linux From 4da09635b015babf1bc4891f39dfc5d1f0df77fe Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 24 Apr 2026 16:36:09 +0100 Subject: [PATCH 28/33] Agents: Adjust padding and border radius for harness dropdown button (#312359) * style: adjust padding and border radius for harness dropdown button * Update src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: mrleemurray Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../aiCustomization/media/aiCustomizationManagement.css | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 b1943a20d8661..addcea886cfe4 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -208,10 +208,9 @@ align-items: center; gap: 6px; width: 100%; - padding: 5px 8px; + padding: 3px 6px; border: 1px solid var(--vscode-dropdown-border, transparent); - border-radius: 4px; - background: var(--vscode-dropdown-background); + border-radius: 6px; color: var(--vscode-dropdown-foreground); cursor: pointer; font-size: 12px; @@ -220,7 +219,6 @@ .ai-customization-management-editor .harness-dropdown-button:hover { background: var(--vscode-dropdown-background); - border-color: var(--vscode-focusBorder); } .ai-customization-management-editor .harness-dropdown-button:focus-visible { From e4a94669538e55a39fbf59bc5eb0531eadbea37f Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:36:25 +0200 Subject: [PATCH 29/33] Add error telemetry for GetPullRequestGlobal (#312362) --- .../src/platform/github/common/githubAPI.ts | 58 +++++++++++++++++++ .../github/common/octoKitServiceImpl.ts | 40 +------------ 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/extensions/copilot/src/platform/github/common/githubAPI.ts b/extensions/copilot/src/platform/github/common/githubAPI.ts index 86b9893e182d2..04a110a2d98db 100644 --- a/extensions/copilot/src/platform/github/common/githubAPI.ts +++ b/extensions/copilot/src/platform/github/common/githubAPI.ts @@ -7,6 +7,46 @@ import { ILogService } from '../../log/common/logService'; import { IFetcherService } from '../../networking/common/fetcherService'; import { ITelemetryService } from '../../telemetry/common/telemetry'; +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +export function getErrorCode(e: unknown): string | undefined { + if (!isObject(e)) { + return undefined; + } + + if (e.status !== undefined) { + return String(e.status); + } + + const networkError = e.networkError; + if (isObject(networkError) && networkError.statusCode !== undefined) { + return String(networkError.statusCode); + } + + const graphQLErrors = e.graphQLErrors; + if (Array.isArray(graphQLErrors)) { + const firstGraphQLError = graphQLErrors[0]; + if (isObject(firstGraphQLError)) { + const extensions = firstGraphQLError.extensions; + if (isObject(extensions) && extensions.code !== undefined) { + return String(extensions.code); + } + } + } + + if (e.code !== undefined) { + return String(e.code); + } + + if (typeof e.name === 'string' && e.name) { + return e.name; + } + + return undefined; +} + export interface PullRequestSearchItem { id: string; number: number; @@ -366,6 +406,24 @@ export async function getPullRequestFromGlobalId( const node = result?.data?.node; logService.debug(`[GitHubAPI] GetPullRequestGlobal: host=${host}, globalId=${globalId}, found=${!!node}, prNumber=${node?.number}, errors=${JSON.stringify(result?.errors)}`); + + if (!node) { + const properties: { errorCode?: string; requestFailed: string } = { + requestFailed: String(result === undefined), + }; + const errorCode = getErrorCode(result?.errors?.[0]); + if (errorCode) { + properties.errorCode = errorCode; + } + /* __GDPR__ + "pr.getPullRequestFromGlobalIdFailed" : { + "errorCode": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "requestFailed": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } + */ + telemetry.sendMSFTTelemetryErrorEvent('pr.getPullRequestFromGlobalIdFailed', properties); + } + return node; } diff --git a/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts b/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts index 2e7de8a268a14..f88d10f160f6c 100644 --- a/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts +++ b/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts @@ -9,7 +9,7 @@ import { ICAPIClientService } from '../../endpoint/common/capiClient'; import { ILogService } from '../../log/common/logService'; import { IFetcherService } from '../../networking/common/fetcherService'; import { ITelemetryService } from '../../telemetry/common/telemetry'; -import { AssignableActor, getAssignableActorsWithAssignableUsers, getAssignableActorsWithSuggestedActors, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI'; +import { AssignableActor, getAssignableActorsWithAssignableUsers, getAssignableActorsWithSuggestedActors, getErrorCode, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI'; import { AuthOptions, BaseOctoKitService, CCAEnabledResult, CustomAgentDetails, CustomAgentListItem, CustomAgentListOptions, ErrorResponseWithStatusCode, IOctoKitService, IOctoKitUser, JobInfo, PermissiveAuthRequiredError, PullRequestFile, RemoteAgentJobResponse } from './githubService'; export class OctoKitService extends BaseOctoKitService implements IOctoKitService { @@ -556,42 +556,4 @@ export class OctoKitService extends BaseOctoKitService implements IOctoKitServic } } -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - -export function getErrorCode(e: unknown): string | undefined { - if (!isObject(e)) { - return undefined; - } - - if (e.status !== undefined) { - return String(e.status); - } - - const networkError = e.networkError; - if (isObject(networkError) && networkError.statusCode !== undefined) { - return String(networkError.statusCode); - } - - const graphQLErrors = e.graphQLErrors; - if (Array.isArray(graphQLErrors)) { - const firstGraphQLError = graphQLErrors[0]; - if (isObject(firstGraphQLError)) { - const extensions = firstGraphQLError.extensions; - if (isObject(extensions) && extensions.code !== undefined) { - return String(extensions.code); - } - } - } - - if (e.code !== undefined) { - return String(e.code); - } - - if (typeof e.name === 'string' && e.name) { - return e.name; - } - return undefined; -} From aa05d1f6b2bb91a6893b63f1756aed54feb87027 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Fri, 24 Apr 2026 20:37:01 +0500 Subject: [PATCH 30/33] sessions: register native IPluginGitService for desktop (#312346) The sessions desktop app was falling through to BrowserPluginGitCommandService (a stub that throws on every call) because the native override was never registered. This broke marketplace plugin install/update which require git operations (clone, pull, fetch). Register NativePluginGitCommandService and its ILocalGitService dependency in sessions.desktop.main.ts, mirroring the existing pattern in workbench.desktop.main.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/sessions.desktop.main.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 0411c189ded57..351500b090211 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -94,12 +94,21 @@ import '../workbench/services/browserView/electron-browser/playwrightWorkbenchSe import '../workbench/services/process/electron-browser/processService.js'; import '../workbench/services/power/electron-browser/powerService.js'; -import { registerSingleton } from '../platform/instantiation/common/extensions.js'; +import { ILocalGitService } from '../platform/git/common/localGitService.js'; +import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; +import { registerSharedProcessRemoteService } from '../platform/ipc/electron-browser/services.js'; +import { IPluginGitService } from '../workbench/contrib/chat/common/plugins/pluginGitService.js'; +import { NativePluginGitCommandService } from '../workbench/contrib/chat/electron-browser/pluginGitCommandService.js'; import { IUserDataInitializationService, UserDataInitializationService } from '../workbench/services/userData/browser/userDataInit.js'; import { SyncDescriptor } from '../platform/instantiation/common/descriptors.js'; registerSingleton(IUserDataInitializationService, new SyncDescriptor(UserDataInitializationService, [[]], true)); +// Override the browser PluginGitCommandService with the native one that always +// runs git locally via the shared process. +registerSingleton(IPluginGitService, NativePluginGitCommandService, InstantiationType.Delayed); +registerSharedProcessRemoteService(ILocalGitService, 'localGit'); + //#endregion From dd180396c628ee4429e595e8f803249a1e21607f Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 24 Apr 2026 11:53:43 -0400 Subject: [PATCH 31/33] Fix inconsistent model picker hover (#312378) * Fix hover collision * Fix hover sizing * Fix bad descriptions --- .../common/languageModelAccess.ts | 27 ++++++++----------- .../actionWidget/browser/actionWidget.css | 4 +-- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts index 0f21892246fd4..b10422f8f3da2 100644 --- a/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts @@ -32,23 +32,21 @@ export function getModelCapabilitiesDescription(endpoint: IChatEndpoint | Langua // 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('mini')) { + return l10n.t('Lightweight GPT model for quick responses and simple tasks with low latency.'); + } + if (name.includes('copilot')) { + return l10n.t('GPT model fine-tuned for Copilot code completions.'); + } 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')) { + if (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.'); - } + return l10n.t('OpenAI GPT model for coding and general assistance.'); } // Gemini models @@ -62,12 +60,9 @@ export function getModelCapabilitiesDescription(endpoint: IChatEndpoint | Langua 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.'); + // Grok models + if (family.includes('grok') || name.includes('grok')) { + return l10n.t('xAI Grok model optimized for fast code generation and development tasks.'); } return undefined; diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 7979a1ef31007..18b5b0ba26cc5 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -389,14 +389,14 @@ border-radius: 5px; box-shadow: 0 2px 8px var(--vscode-widget-shadow); z-index: 50; - width: fit-content; + width: max-content; } .action-list-submenu-hover-header { padding: 4px 8px; line-height: 1.4em; font-size: 12px; - max-width: var(--vscode-hover-maxWidth, 500px); + max-width: 300px; word-wrap: break-word; } From 0f0b735a574577c026e03dbeefeb6e66a240a1b2 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 24 Apr 2026 16:56:43 +0100 Subject: [PATCH 32/33] Agents: Implement scrollable container for welcome prompts (#312357) * chat: implement scrollable container for welcome prompts * refactor: improve scrollbar resize observation logic in PromptLaunchersAICustomizationWelcomePage * fix: update scroll dimensions computation and adjust CSS for relative positioning --------- Co-authored-by: mrleemurray --- ...CustomizationWelcomePagePromptLaunchers.ts | 22 ++++++++++++++++++- .../aiCustomizationWelcomePromptLaunchers.css | 14 ++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts index 685f9e226f786..bf7bfb1909455 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts @@ -5,6 +5,8 @@ import './media/aiCustomizationWelcomePromptLaunchers.css'; import * as DOM from '../../../../../base/browser/dom.js'; +import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../nls.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -33,6 +35,7 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem private readonly cardDisposables = this._register(new DisposableStore()); readonly container: HTMLElement; + private readonly scrollable: DomScrollableElement; private cardsContainer: HTMLElement | undefined; private inputElement: HTMLInputElement | undefined; @@ -93,7 +96,21 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem ) { super(); - this.container = DOM.append(parent, $('.welcome-prompts-content-container')); + this.container = $('.welcome-prompts-content-container'); + this.scrollable = this._register(new DomScrollableElement(this.container, { + horizontal: ScrollbarVisibility.Hidden, + vertical: ScrollbarVisibility.Auto, + useShadows: false, + })); + const scrollableNode = this.scrollable.getDomNode(); + scrollableNode.classList.add('welcome-prompts-scrollable'); + parent.appendChild(scrollableNode); + + // Re-scan whenever the wrapper changes size so the scrollbar reflects + // the current overflow state. rebuildCards() scans after content changes. + const resizeObserver = this._register(new DOM.DisposableResizeObserver(() => this.scrollable.scanDomNode())); + this._register(resizeObserver.observe(scrollableNode)); + const welcomeInner = DOM.append(this.container, $('.welcome-prompts-inner')); const heading = DOM.append(welcomeInner, $('h2.welcome-prompts-heading')); @@ -256,6 +273,9 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem } })); } + + // Content changed — recompute scroll dimensions. + this.scrollable.scanDomNode(); } focus(): void { 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 e076c82edcae5..ba32a4d4a39f9 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css @@ -3,11 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.ai-customization-management-editor .welcome-prompts-content-container { +/* The DomScrollableElement wrapper fills the welcome page host */ +.ai-customization-management-editor .welcome-prompts-scrollable { + position: relative; + width: 100%; height: 100%; - overflow-y: auto; +} + +/* The inner element is the one DomScrollableElement scrolls. + * Constrain it so clientHeight < scrollHeight when content overflows. */ +.ai-customization-management-editor .welcome-prompts-content-container { + position: absolute; + inset: 0; padding: 24px; box-sizing: border-box; + overflow: hidden; } .ai-customization-management-editor .welcome-prompts-inner { From 9a6329f39503c3ca711af3d62adc79cd5e2989cb Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 24 Apr 2026 16:56:47 +0100 Subject: [PATCH 33/33] Agents: Adjust dimensions and alignment for chat input buttons (#312376) style: adjust dimensions and alignment for chat input buttons Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../contrib/chat/browser/media/chatInput.css | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/media/chatInput.css b/src/vs/sessions/contrib/chat/browser/media/chatInput.css index 9d7c730919321..e933f0e5f9363 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatInput.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatInput.css @@ -239,8 +239,8 @@ justify-content: center; flex-shrink: 0; position: relative; - width: 22px; - height: 22px; + width: 23px; + height: 23px; border-radius: 4px; } @@ -248,9 +248,9 @@ display: flex; align-items: center; justify-content: center; - width: 22px; - height: 22px; - min-width: 22px; + width: 23px; + height: 23px; + min-width: 23px; padding: 0; border-radius: 4px; color: var(--vscode-icon-foreground); @@ -282,8 +282,11 @@ box-shadow: none !important; } -.monaco-workbench .sessions-chat-send-button .monaco-button .codicon[class*='codicon-'] { - font-size: 14px; +.monaco-workbench .sessions-chat-send-button .monaco-button.codicon[class*='codicon-']::before, +.monaco-workbench .sessions-chat-send-button .monaco-button .codicon[class*='codicon-']::before { + /* Optical alignment: nudge arrow glyph 1px left to visually center it. */ + display: inline-block; + transform: translateX(-0.5px); } /* Ensure no underline / link decoration ever shows under the codicon glyph @@ -352,9 +355,17 @@ } .agent-sessions-workbench .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up) > .action-label.codicon-arrow-up { + box-sizing: border-box; + width: 23px; + height: 23px; transition: background-color 250ms ease, color 250ms ease; } +/* Optical alignment: nudge arrow glyph 1px left to visually center it. */ +.agent-sessions-workbench .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item > .action-label.codicon-arrow-up::before { + display: inline-block; +} + /* Focus indicator drawn on the action-item wrapper so it sits cleanly around the button surface with a small offset, matching the new-session button. */ .agent-sessions-workbench .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:focus-visible) {