From a06e08f5e9d5518638aba87b72ed6b7bf0d399bc Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Sun, 7 Dec 2025 10:45:47 +0100 Subject: [PATCH 01/37] fix: memory leak in debug session --- .../workbench/contrib/debug/browser/debugSession.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 34cf1145a7ad..0a1e7433b375 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -50,7 +50,7 @@ import { RawDebugSession } from './rawDebugSession.js'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; -export class DebugSession implements IDebugSession { +export class DebugSession extends Disposable implements IDebugSession { parentSession: IDebugSession | undefined; rememberedCapabilities?: DebugProtocol.Capabilities; @@ -122,6 +122,7 @@ export class DebugSession implements IDebugSession { @ITestResultService testResultService: ITestResultService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { + super(); this._options = options || {}; this.parentSession = this._options.parentSession; if (this.hasSeparateRepl()) { @@ -1494,13 +1495,19 @@ export class DebugSession implements IDebugSession { this.passFocusScheduler.cancel(); this.passFocusScheduler.dispose(); this.model.clearThreads(this.getId(), true); + this.sources.clear(); + this.threads.clear(); + this.threadIds = []; + this.stoppedDetails = []; this._onDidChangeState.fire(); } - public dispose() { + override dispose() { this.cancelAllRequests(); this.rawListeners.dispose(); this.globalDisposables.dispose(); + this._waitToResume = undefined; + super.dispose(); } //---- sources From 4e54be8078c750c33485a3b926d48ef9d5d184bc Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Tue, 16 Dec 2025 18:55:11 +0100 Subject: [PATCH 02/37] undo change --- src/vs/workbench/contrib/debug/browser/debugSession.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 0a1e7433b375..fa9b79a53b61 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -50,7 +50,7 @@ import { RawDebugSession } from './rawDebugSession.js'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; -export class DebugSession extends Disposable implements IDebugSession { +export class DebugSession implements IDebugSession { parentSession: IDebugSession | undefined; rememberedCapabilities?: DebugProtocol.Capabilities; @@ -122,7 +122,6 @@ export class DebugSession extends Disposable implements IDebugSession { @ITestResultService testResultService: ITestResultService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { - super(); this._options = options || {}; this.parentSession = this._options.parentSession; if (this.hasSeparateRepl()) { @@ -1502,12 +1501,11 @@ export class DebugSession extends Disposable implements IDebugSession { this._onDidChangeState.fire(); } - override dispose() { + public dispose() { this.cancelAllRequests(); this.rawListeners.dispose(); this.globalDisposables.dispose(); this._waitToResume = undefined; - super.dispose(); } //---- sources From 3168a3ee560fa8e4e80b74992d6af426faeab855 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 16 Dec 2025 18:00:35 +0000 Subject: [PATCH 03/37] feat(codicons): add worktree icons to codicons library --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 123452 -> 124076 bytes src/vs/base/common/codiconsLibrary.ts | 2 ++ 2 files changed, 2 insertions(+) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index db3a7151b2832c9a2363fd4b646495ff5a889a20..1c5cb36f7dfb340713954e3cadaac9510d29d09e 100644 GIT binary patch delta 930 zcmX|7O-vI}5T4ohy4&r~cDLJh%eF-{t=a1bWV0-8Jor;v+U`VuwGkz8nR8YWE;Y7pwSm@vSXF|d3&61s zlrB~-yBct|a?90=9o1D=mLoE!RChePNI8k~RktJs#SSLVJ}zAS>Ff4-Wq@z+2&HowqhHk(uONwn zgQwIZ=>~gx6#^q_S(Go={I?vp}uWk8J} z24y)IX^mJQDw@LnnA^Y@Sw delta 357 zcmZ2;l6}t+_6Y{{Z%i8m7#O&87#QTPrRP+pb^q|+#=xMf!N53mLq=+1O2niEK@1EG z4}jP#11P{0#%v8_?*U?!jNFon%((wg85kJfFfed9u00SdX zEf81i=WLny#*k59vm7HI=VTKuVe@@pIR=&;49sAb4v^+zU|`+Hz{1i4)Fr|o02E<) z)$yw5)$vy+Uvs|pdlUDjW^x^uJ)_R%eO#tY%zRAzo4@gVV%+?OPmqxlkcMmgWFf$5lcRS9gz_LB- ZG^2>57<*NIey(n29tW7tFD(HZ3ji=SWRw5^ diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 66b1ed5c2e4b..29037850602d 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -648,4 +648,6 @@ export const codiconsLibrary = { cloudSmall: register('cloud-small', 0xec7a), addSmall: register('add-small', 0xec7b), removeSmall: register('remove-small', 0xec7c), + worktreeSmall: register('worktree-small', 0xec7d), + worktree: register('worktree', 0xec7e), } as const; From cbf7676898ca6a8f7ef1f06d5b38f9cf0b2ecfc8 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 16 Dec 2025 12:18:25 -0600 Subject: [PATCH 04/37] Prevent alt buffer entrance from hanging chat agent (#282883) fix #280705 --- .../executeStrategy/basicExecuteStrategy.ts | 14 +++- .../executeStrategy/executeStrategy.ts | 1 + .../executeStrategy/noneExecuteStrategy.ts | 19 ++++- .../executeStrategy/richExecuteStrategy.ts | 14 +++- .../executeStrategy/strategyHelpers.ts | 27 +++++++ .../browser/tools/runInTerminalTool.ts | 71 +++++++++++++------ 6 files changed, 118 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts index e8bb0eab4a38..25e6f69fc8fd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts @@ -13,7 +13,7 @@ import { ITerminalLogService } from '../../../../../../platform/terminal/common/ import { trackIdleOnPrompt, waitForIdle, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; import { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; -import { setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; /** * This strategy is used when shell integration is enabled, but rich command detection was not @@ -92,6 +92,7 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { if (!xterm) { throw new Error('Xterm is not available'); } + const alternateBufferPromise = createAltBufferPromise(xterm, store, this._log.bind(this)); // Wait for the terminal to idle before executing the command this._log('Waiting for idle'); @@ -126,10 +127,19 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { // Wait for the next end execution event - note that this may not correspond to the actual // execution requested this._log('Waiting for done event'); - const onDoneResult = await onDone; + const onDoneResult = await Promise.race([onDone, alternateBufferPromise.then(() => ({ type: 'alternateBuffer' } as const))]); if (onDoneResult && onDoneResult.type === 'disposal') { throw new Error('The terminal was closed'); } + if (onDoneResult && onDoneResult.type === 'alternateBuffer') { + this._log('Detected alternate buffer entry, skipping output capture'); + return { + output: undefined, + exitCode: undefined, + error: 'alternateBuffer', + didEnterAltBuffer: true + }; + } const finishedCommand = onDoneResult && onDoneResult.type === 'success' ? onDoneResult.command : undefined; if (finishedCommand) { this._log(`Finished command id=${finishedCommand.id ?? 'none'} for requested=${commandId ?? 'none'}`); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts index dbfbd85b2225..cae93dc83b7b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts @@ -29,6 +29,7 @@ export interface ITerminalExecuteStrategyResult { additionalInformation?: string; exitCode?: number; error?: string; + didEnterAltBuffer?: boolean; } export async function waitForIdle(onData: Event, idleDurationMs: number): Promise { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts index 8dc54fd8a53e..523906025b7a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts @@ -11,7 +11,7 @@ import { ITerminalLogService } from '../../../../../../platform/terminal/common/ import { waitForIdle, waitForIdleWithPromptHeuristics, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; import { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; -import { setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; /** * This strategy is used when no shell integration is available. There are very few extension APIs @@ -47,6 +47,7 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { if (!xterm) { throw new Error('Xterm is not available'); } + const alternateBufferPromise = createAltBufferPromise(xterm, store, this._log.bind(this)); // Wait for the terminal to idle before executing the command this._log('Waiting for idle'); @@ -79,7 +80,21 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { // Assume the command is done when it's idle this._log('Waiting for idle with prompt heuristics'); - const promptResult = await waitForIdleWithPromptHeuristics(this._instance.onData, this._instance, 1000, 10000); + const promptResultOrAltBuffer = await Promise.race([ + waitForIdleWithPromptHeuristics(this._instance.onData, this._instance, 1000, 10000), + alternateBufferPromise.then(() => 'alternateBuffer' as const) + ]); + if (promptResultOrAltBuffer === 'alternateBuffer') { + this._log('Detected alternate buffer entry, skipping output capture'); + return { + output: undefined, + additionalInformation: undefined, + exitCode: undefined, + error: 'alternateBuffer', + didEnterAltBuffer: true, + }; + } + const promptResult = promptResultOrAltBuffer; this._log(`Prompt detection result: ${promptResult.detected ? 'detected' : 'not detected'} - ${promptResult.reason}`); if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts index 8417bcb01a81..c65b53492d6d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts @@ -13,7 +13,7 @@ import { ITerminalLogService } from '../../../../../../platform/terminal/common/ import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import { trackIdleOnPrompt, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; -import { setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; /** * This strategy is used when the terminal has rich shell integration/command detection is @@ -45,6 +45,7 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { if (!xterm) { throw new Error('Xterm is not available'); } + const alternateBufferPromise = createAltBufferPromise(xterm, store, this._log.bind(this)); const onDone = Promise.race([ Event.toPromise(this._commandDetection.onCommandFinished, store).then(e => { @@ -80,10 +81,19 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { // Wait for the terminal to idle this._log('Waiting for done event'); - const onDoneResult = await onDone; + const onDoneResult = await Promise.race([onDone, alternateBufferPromise.then(() => ({ type: 'alternateBuffer' } as const))]); if (onDoneResult && onDoneResult.type === 'disposal') { throw new Error('The terminal was closed'); } + if (onDoneResult && onDoneResult.type === 'alternateBuffer') { + this._log('Detected alternate buffer entry, skipping output capture'); + return { + output: undefined, + exitCode: undefined, + error: 'alternateBuffer', + didEnterAltBuffer: true + }; + } const finishedCommand = onDoneResult && onDoneResult.type === 'success' ? onDoneResult.command : undefined; if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts index 3ab743aa2b61..5c63b233ec26 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from '../../../../../../base/common/async.js'; import { DisposableStore, MutableDisposable, toDisposable, type IDisposable } from '../../../../../../base/common/lifecycle.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; @@ -43,3 +44,29 @@ export function setupRecreatingStartMarker( })); store.add(startMarker); } + +export function createAltBufferPromise( + xterm: { raw: { buffer: { active: unknown; alternate: unknown; onBufferChange: (callback: () => void) => IDisposable } } }, + store: DisposableStore, + log?: (message: string) => void, +): Promise { + const deferred = new DeferredPromise(); + const complete = () => { + if (!deferred.isSettled) { + log?.('Detected alternate buffer entry'); + deferred.complete(); + } + }; + + if (xterm.raw.buffer.active === xterm.raw.buffer.alternate) { + complete(); + } else { + store.add(xterm.raw.buffer.onBufferChange(() => { + if (xterm.raw.buffer.active === xterm.raw.buffer.alternate) { + complete(); + } + })); + } + + return deferred.p; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 755050cdc4c4..82864a5b1866 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -5,7 +5,7 @@ import type { IMarker as IXtermMarker } from '@xterm/xterm'; import { timeout } from '../../../../../../base/common/async.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { Event } from '../../../../../../base/common/event.js'; @@ -257,6 +257,8 @@ const telemetryIgnoredSequences = [ '\x1b[O', // Focus out ]; +const altBufferMessage = localize('runInTerminalTool.altBufferMessage', "The command opened the alternate buffer."); + export class RunInTerminalTool extends Disposable implements IToolImpl { @@ -639,6 +641,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let outputLineCount = -1; let exitCode: number | undefined; + let altBufferResult: IToolResult | undefined; + const executeCancellation = store.add(new CancellationTokenSource(token)); try { let strategy: ITerminalExecuteStrategy; switch (toolTerminal.shellIntegrationQuality) { @@ -662,39 +666,58 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, { instance: toolTerminal.instance, sessionId: invocation.context?.sessionId, getOutput: (marker?: IXtermMarker) => getOutput(toolTerminal.instance, marker ?? startMarker) }, undefined, invocation.context, token, command)); } })); - const executeResult = await strategy.execute(command, token, commandId); + const executeResult = await strategy.execute(command, executeCancellation.token, commandId); // Reset user input state after command execution completes toolTerminal.receivedUserInput = false; if (token.isCancellationRequested) { throw new CancellationError(); } - await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId); - { + if (executeResult.didEnterAltBuffer) { const state = toolSpecificData.terminalCommandState ?? {}; state.timestamp = state.timestamp ?? timingStart; - if (executeResult.exitCode !== undefined) { - state.exitCode = executeResult.exitCode; - if (state.timestamp !== undefined) { - state.duration = state.duration ?? Math.max(0, Date.now() - state.timestamp); - } - } toolSpecificData.terminalCommandState = state; - } + toolResultMessage = altBufferMessage; + outputLineCount = 0; + error = executeResult.error ?? 'alternateBuffer'; + altBufferResult = { + toolResultMessage, + toolMetadata: { + exitCode: undefined + }, + content: [{ + kind: 'text', + value: altBufferMessage, + }] + }; + } else { + await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId); + { + const state = toolSpecificData.terminalCommandState ?? {}; + state.timestamp = state.timestamp ?? timingStart; + if (executeResult.exitCode !== undefined) { + state.exitCode = executeResult.exitCode; + if (state.timestamp !== undefined) { + state.duration = state.duration ?? Math.max(0, Date.now() - state.timestamp); + } + } + toolSpecificData.terminalCommandState = state; + } - this._logService.debug(`RunInTerminalTool: Finished \`${strategy.type}\` execute strategy with exitCode \`${executeResult.exitCode}\`, result.length \`${executeResult.output?.length}\`, error \`${executeResult.error}\``); - outputLineCount = executeResult.output === undefined ? 0 : count(executeResult.output.trim(), '\n') + 1; - exitCode = executeResult.exitCode; - error = executeResult.error; + this._logService.debug(`RunInTerminalTool: Finished \`${strategy.type}\` execute strategy with exitCode \`${executeResult.exitCode}\`, result.length \`${executeResult.output?.length}\`, error \`${executeResult.error}\``); + outputLineCount = executeResult.output === undefined ? 0 : count(executeResult.output.trim(), '\n') + 1; + exitCode = executeResult.exitCode; + error = executeResult.error; - const resultArr: string[] = []; - if (executeResult.output !== undefined) { - resultArr.push(executeResult.output); + const resultArr: string[] = []; + if (executeResult.output !== undefined) { + resultArr.push(executeResult.output); + } + if (executeResult.additionalInformation) { + resultArr.push(executeResult.additionalInformation); + } + terminalResult = resultArr.join('\n\n'); } - if (executeResult.additionalInformation) { - resultArr.push(executeResult.additionalInformation); - } - terminalResult = resultArr.join('\n\n'); } catch (e) { this._logService.debug(`RunInTerminalTool: Threw exception`); @@ -731,6 +754,10 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }); } + if (altBufferResult) { + return altBufferResult; + } + const resultText: string[] = []; if (didUserEditCommand) { resultText.push(`Note: The user manually edited the command to \`${command}\`, and this is the output of running that command instead:\n`); From ffd0bd291c2c521488ffd64f841a17039bcffab5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:27:22 -0800 Subject: [PATCH 05/37] Fix duplicated Advanced tag in settings editor (#283852) * Initial plan * Fix duplicated Advanced tag in settings editor Remove advanced tag handling from updatePreviewIndicator() since it's already handled by updateAdvancedIndicator(). This prevents the "Advanced" label from appearing twice for settings with the advanced tag. Fixes duplicate Advanced tag display issue Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- .../browser/settingsEditorSettingIndicators.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index e5de36909fb8..47874f09ade6 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -345,15 +345,12 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { updatePreviewIndicator(element: SettingsTreeSettingElement) { const isPreviewSetting = element.tags?.has('preview'); const isExperimentalSetting = element.tags?.has('experimental'); - const isAdvancedSetting = element.tags?.has('advanced'); - this.previewIndicator.element.style.display = (isPreviewSetting || isExperimentalSetting || isAdvancedSetting) ? 'inline' : 'none'; + this.previewIndicator.element.style.display = (isPreviewSetting || isExperimentalSetting) ? 'inline' : 'none'; this.previewIndicator.label.text = isPreviewSetting ? localize('previewLabel', "Preview") : - isExperimentalSetting ? - localize('experimentalLabel', "Experimental") : - localize('advancedLabel', "Advanced"); + localize('experimentalLabel', "Experimental"); - const content = isPreviewSetting ? PREVIEW_INDICATOR_DESCRIPTION : isExperimentalSetting ? EXPERIMENTAL_INDICATOR_DESCRIPTION : ADVANCED_INDICATOR_DESCRIPTION; + const content = isPreviewSetting ? PREVIEW_INDICATOR_DESCRIPTION : EXPERIMENTAL_INDICATOR_DESCRIPTION; const showHover = (focus: boolean) => { return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, From 5fd25940173706ecaab09fd7da1d56e933b47f29 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 16 Dec 2025 12:42:53 -0600 Subject: [PATCH 06/37] provide completions for git remotes (#241675) --- .../terminal-suggest/src/completions/git.ts | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/git.ts b/extensions/terminal-suggest/src/completions/git.ts index ff11171f9dae..68cc6fda2d7b 100644 --- a/extensions/terminal-suggest/src/completions/git.ts +++ b/extensions/terminal-suggest/src/completions/git.ts @@ -99,6 +99,7 @@ const postProcessBranches = } let description = "Branch"; + if (insertWithoutRemotes && name.startsWith("remotes/")) { name = name.slice(name.indexOf("/", 8) + 1); description = "Remote branch"; @@ -287,7 +288,7 @@ export const gitGenerators = { "refs/remotes/", ], postProcess: postProcessBranches({ insertWithoutRemotes: true }), - } satisfies Fig.Generator, + }, localBranches: { script: [ @@ -295,26 +296,44 @@ export const gitGenerators = { "refs/heads/", ], postProcess: postProcessBranches({ insertWithoutRemotes: true }), - } satisfies Fig.Generator, + }, // custom generator to display local branches by default or // remote branches if '-r' flag is used. See branch -d for use localOrRemoteBranches: { custom: async (tokens, executeShellCommand) => { const pp = postProcessBranches({ insertWithoutRemotes: true }); - const refs = tokens.includes("-r") ? "refs/remotes/" : "refs/heads/"; - return pp?.( - ( - await executeShellCommand({ - command: gitBranchForEachRefArgs[0], - args: [ - ...gitBranchForEachRefArgs.slice(1), - refs, - ], - }) - ).stdout, - tokens - ); + if (tokens.includes("-r")) { + return pp?.( + ( + await executeShellCommand({ + command: "git", + args: [ + "--no-optional-locks", + "-r", + "--no-color", + "--sort=-committerdate", + ], + }) + ).stdout, + tokens + ); + } else { + return pp?.( + ( + await executeShellCommand({ + command: "git", + args: [ + "--no-optional-locks", + "branch", + "--no-color", + "--sort=-committerdate", + ], + }) + ).stdout, + tokens + ); + } }, } satisfies Fig.Generator, From f4367f568f22aff90295edb026ab877eb0d61cc0 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 16 Dec 2025 12:43:58 -0600 Subject: [PATCH 07/37] wait for xterm element vs assuming it's defined (#283868) fixes #283287 --- .../browser/terminal.suggest.contribution.ts | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index cf7c936e423d..ef1664857418 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -155,19 +155,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo xterm.loadAddon(addon); this._loadLspCompletionAddon(xterm); - let container: HTMLElement | null = null; - if (this._ctx.instance.target === TerminalLocation.Editor) { - container = xterm.element!; - } else { - container = dom.findParentWithClass(xterm.element!, 'panel'); - if (!container) { - // Fallback for sidebar or unknown location - container = xterm.element!; - } - } - addon.setContainerWithOverflow(container); - // eslint-disable-next-line no-restricted-syntax - addon.setScreen(xterm.element!.querySelector('.xterm-screen')!); + this._prepareAddonLayout(xterm); this.add(dom.addDisposableListener(this._ctx.instance.domElement, dom.EventType.FOCUS_OUT, (e) => { const focusedElement = e.relatedTarget as HTMLElement; @@ -216,20 +204,53 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo return; } - const xtermElement = this._ctx.instance.xterm.raw.element; - if (!xtermElement) { + this._prepareAddonLayout(this._ctx.instance.xterm.raw); + } + + + private async _prepareAddonLayout(xterm: RawXtermTerminal): Promise { + const addon = this._addon.value; + if (!addon || this.isDisposed) { return; } - // Update the container based on the new target location - if (target === TerminalLocation.Editor) { - addon.setContainerWithOverflow(xtermElement); - } else { - const panelContainer = dom.findParentWithClass(xtermElement, 'panel'); - if (panelContainer) { - addon.setContainerWithOverflow(panelContainer); - } + const xtermElement = xterm.element ?? await this._waitForXtermElement(xterm); + if (!xtermElement || this.isDisposed || addon !== this._addon.value) { + return; } + + const container = this._resolveAddonContainer(xtermElement); + addon.setContainerWithOverflow(container); + // eslint-disable-next-line no-restricted-syntax + const screenElement = xtermElement.querySelector('.xterm-screen'); + if (dom.isHTMLElement(screenElement)) { + addon.setScreen(screenElement); + } + } + + private async _waitForXtermElement(xterm: RawXtermTerminal): Promise { + if (xterm.element) { + return xterm.element; + } + + await Promise.race([ + Event.toPromise(Event.filter(this._ctx.instance.onDidChangeVisibility, visible => visible)), + Event.toPromise(this._ctx.instance.onDisposed) + ]); + + if (this.isDisposed || this._ctx.instance.isDisposed) { + return undefined; + } + + return xterm.element ?? undefined; + } + + private _resolveAddonContainer(xtermElement: HTMLElement): HTMLElement { + if (this._ctx.instance.target === TerminalLocation.Editor) { + return xtermElement; + } + + return dom.findParentWithClass(xtermElement, 'panel') ?? xtermElement; } } From c79dbd0432433dde5697606122583c5c94d0ab74 Mon Sep 17 00:00:00 2001 From: Michael Lively <12552271+Yoyokrazy@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:50:13 -0800 Subject: [PATCH 08/37] Add migrate prompt for grooming help (#283867) add migrate prompt for grooming help --- .github/prompts/migrate.prompt.md | 184 ++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 .github/prompts/migrate.prompt.md diff --git a/.github/prompts/migrate.prompt.md b/.github/prompts/migrate.prompt.md new file mode 100644 index 000000000000..d404ebf6f4be --- /dev/null +++ b/.github/prompts/migrate.prompt.md @@ -0,0 +1,184 @@ +--- +agent: agent +tools: + [ + "github/add_issue_comment", + "github/get_label", + "github/get_me", + "github/issue_read", + "github/issue_write", + "github/search_issues", + "github/search_pull_requests", + "github/search_repositories", + "github/sub_issue_write", + ] +--- + +# Issue Migration Prompt + +Use this prompt when migrating issues from one GitHub repository to another (e.g., from `microsoft/vscode-copilot` to `microsoft/vscode`). + +## Input Methods + +You can specify which issues to migrate using **any** of these three methods: + +### Option A: GitHub Search Query URL + +Provide a full GitHub issues search URL. **All matching issues will be migrated.** + +``` +https://github.com/microsoft/vscode-copilot/issues?q=is%3Aissue+is%3Aopen+assignee%3Ayoyokrazy +``` + +### Option B: GitHub Search Query Parameters + +Provide search query syntax for a specific repo. **All matching issues will be migrated.** + +``` +repo:microsoft/vscode-copilot is:issue is:open assignee:yoyokrazy +``` + +Common query filters: + +- `is:issue` / `is:pr` - Filter by type +- `is:open` / `is:closed` - Filter by state +- `assignee:USERNAME` - Filter by assignee +- `author:USERNAME` - Filter by author +- `label:LABEL` - Filter by label +- `milestone:MILESTONE` - Filter by milestone + +### Option C: Specific Issue URL + +Provide a direct link to a single issue. **Only this issue will be migrated.** + +``` +https://github.com/microsoft/vscode-copilot/issues/12345 +``` + +## Task + +**Target Repository:** `{TARGET_REPO}` + +Based on the input provided, migrate the issue(s) to the target repository following all requirements below. + +## Requirements + +### 1. Issue Body Format + +Create the new issue with this header format: + +```markdown +_Transferred from {SOURCE_REPO}#{ORIGINAL_ISSUE_NUMBER}_ +_Original author: `@{ORIGINAL_AUTHOR}`_ + +--- + +{ORIGINAL_ISSUE_BODY} +``` + +### 2. Comment Migration + +For each comment on the original issue, add a comment to the new issue: + +```markdown +_`@{COMMENT_AUTHOR}` commented:_ + +--- + +{COMMENT_BODY} +``` + +### 3. CRITICAL: Preventing GitHub Pings + +**ALL `@username` mentions MUST be wrapped in backticks to prevent GitHub from sending notifications.** + +✅ Correct: `` `@username` `` +❌ Wrong: `@username` + +This applies to: + +- The "Original author" line in the issue body +- Any `@mentions` within the issue body content +- The comment author attribution line +- Any `@mentions` within comment content +- Any quoted content that contains `@mentions` + +### 4. CRITICAL: Issue/PR Link Reformatting + +**Issue references like `#12345` are REPO-SPECIFIC.** If you copy `#12345` from the source repo to the target repo, it will incorrectly link to issue 12345 in the _target_ repo instead of the source. + +**Convert ALL `#NUMBER` references to full URLs:** + +✅ Correct: `https://github.com/microsoft/vscode-copilot/issues/12345` +✅ Also OK: `microsoft/vscode-copilot#12345` +❌ Wrong: `#12345` (will link to wrong repo) + +This applies to: + +- Issue references in the body (`#12345` → full URL) +- PR references in the body (`#12345` → full URL) +- References in comments +- References in quoted content +- References in image alt text or links + +**Exception:** References that are _already_ full URLs should be left unchanged. + +### 5. Metadata Preservation + +- Copy all applicable labels to the new issue +- Assign the new issue to the same assignees (if they exist in target repo) +- Preserve the issue title exactly + +### 5. Post-Migration + +After creating the new issue and all comments: + +- Add a comment to the **original** issue linking to the new issue: + ```markdown + Migrated to {TARGET_REPO}#{NEW_ISSUE_NUMBER} + ``` +- Close the original issue as not_planned + +## Example Transformation + +### Original Issue Body (in `microsoft/vscode-copilot`): + +```markdown +I noticed @johndoe had a similar issue in #9999. cc @janedoe for visibility. + +Related to #8888 and microsoft/vscode#12345. + +Steps to reproduce: + +1. Open VS Code +2. ... +``` + +### Migrated Issue Body (in `microsoft/vscode`): + +```markdown +_Transferred from microsoft/vscode-copilot#12345_ +_Original author: `@originalauthor`_ + +--- + +I noticed `@johndoe` had a similar issue in https://github.com/microsoft/vscode-copilot/issues/9999. cc `@janedoe` for visibility. + +Related to https://github.com/microsoft/vscode-copilot/issues/8888 and microsoft/vscode#12345. + +Steps to reproduce: + +1. Open VS Code +2. ... +``` + +Note: The `microsoft/vscode#12345` reference was already a cross-repo link, so it stays unchanged. + +## Checklist Before Migration + +- [ ] Confirm input method (query URL, query params, or specific issue URL) +- [ ] Confirm target repository +- [ ] If using query: verify the query returns the expected issues +- [ ] Verify all `@mentions` are wrapped in backticks +- [ ] Verify all `#NUMBER` references are converted to full URLs +- [ ] Decide whether to close original issues after migration From 53f4de24cb61db9267fec741b31768c35ce127ae Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Tue, 16 Dec 2025 21:50:37 +0300 Subject: [PATCH 09/37] fix: memory leak in terminal find widget (#283466) --- .../find/browser/terminalFindWidget.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts index 687f3e0f7ad4..5092cb3f998a 100644 --- a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts @@ -15,7 +15,7 @@ import { IKeybindingService } from '../../../../../platform/keybinding/common/ke import { Event } from '../../../../../base/common/event.js'; import type { ISearchOptions } from '@xterm/addon-search'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { TerminalFindCommandId } from '../common/terminal.find.js'; import { TerminalClipboardContribution } from '../../clipboard/browser/terminal.clipboard.contribution.js'; @@ -31,6 +31,7 @@ export class TerminalFindWidget extends SimpleFindWidget { private _findWidgetVisible: IContextKey; private _overrideCopyOnSelectionDisposable: IDisposable | undefined; + private _selectionDisposable = this._register(new MutableDisposable()); constructor( private _instance: ITerminalInstance | IDetachedTerminalInstance, @@ -191,17 +192,19 @@ export class TerminalFindWidget extends SimpleFindWidget { } } + private _registerSelectionChangeListener(xterm: IXtermTerminal): void { + this._selectionDisposable.value = Event.once(xterm.onDidChangeSelection)(() => xterm.clearActiveSearchDecoration()); + } + private async _findNextWithEvent(xterm: IXtermTerminal, term: string, options: ISearchOptions): Promise { - return xterm.findNext(term, options).then(foundMatch => { - this._register(Event.once(xterm.onDidChangeSelection)(() => xterm.clearActiveSearchDecoration())); - return foundMatch; - }); + const foundMatch = await xterm.findNext(term, options); + this._registerSelectionChangeListener(xterm); + return foundMatch; } private async _findPreviousWithEvent(xterm: IXtermTerminal, term: string, options: ISearchOptions): Promise { - return xterm.findPrevious(term, options).then(foundMatch => { - this._register(Event.once(xterm.onDidChangeSelection)(() => xterm.clearActiveSearchDecoration())); - return foundMatch; - }); + const foundMatch = await xterm.findPrevious(term, options); + this._registerSelectionChangeListener(xterm); + return foundMatch; } } From cea4fe5e1c3cb35d0351b6aa98c1f8347c7af0ba Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 16 Dec 2025 13:05:08 -0600 Subject: [PATCH 10/37] fix task disposable leak (#283872) fixes #274750 --- .../workbench/contrib/tasks/browser/abstractTaskService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 30fce10b2d22..6e2ddbcf2f24 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -3015,7 +3015,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return entries; } private async _showTwoLevelQuickPick(placeHolder: string, defaultEntry?: ITaskQuickPickEntry, type?: string, name?: string) { - return this._instantiationService.createInstance(TaskQuickPick).show(placeHolder, defaultEntry, type, name); + const taskQuickPick = this._instantiationService.createInstance(TaskQuickPick); + try { + return await taskQuickPick.show(placeHolder, defaultEntry, type, name); + } finally { + taskQuickPick.dispose(); + } } private async _showQuickPick(tasks: Promise | Task[], placeHolder: string, defaultEntry?: ITaskQuickPickEntry, group: boolean = false, sort: boolean = false, selectedEntry?: ITaskQuickPickEntry, additionalEntries?: ITaskQuickPickEntry[], name?: string): Promise { From 56555a8bf1fdfb89f318da366065236b1852accb Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Tue, 16 Dec 2025 11:11:02 -0800 Subject: [PATCH 11/37] Finalize quickPickItemResource API proposal (#283877) --- extensions/vscode-api-tests/package.json | 1 - .../common/extensionsApiProposals.ts | 3 --- .../workbench/api/common/extHostQuickOpen.ts | 8 ------- src/vscode-dts/vscode.d.ts | 11 ++++++++++ ...vscode.proposed.quickPickItemResource.d.ts | 22 ------------------- 5 files changed, 11 insertions(+), 34 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.quickPickItemResource.d.ts diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index e7eacadec2e3..3f586a847ea6 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -34,7 +34,6 @@ "portsAttributes", "quickInputButtonLocation", "quickPickSortByLabel", - "quickPickItemResource", "resolvers", "scmActionButton", "scmSelectedProvider", diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 959b3477de4a..00ac167445e3 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -334,9 +334,6 @@ const _allApiProposals = { quickInputButtonLocation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts', }, - quickPickItemResource: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemResource.d.ts', - }, quickPickItemTooltip: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', }, diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index ef3f5503ed3a..3cff3d072d36 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -102,10 +102,6 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx checkProposedApiEnabled(extension, 'quickPickItemTooltip'); } - if (item.resourceUri) { - checkProposedApiEnabled(extension, 'quickPickItemResource'); - } - pickItems.push({ label: item.label, iconPathDto: IconPath.from(item.iconPath), @@ -576,10 +572,6 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx checkProposedApiEnabled(this._extension, 'quickPickItemTooltip'); } - if (item.resourceUri) { - checkProposedApiEnabled(this._extension, 'quickPickItemResource'); - } - pickItems.push({ handle, label: item.label, diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 100950c66488..d4eb6321a9d5 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -1938,6 +1938,17 @@ declare module 'vscode' { */ detail?: string; + /** + * A {@link Uri} representing the resource associated with this item. + * + * When set, this property is used to automatically derive several item properties if they are not explicitly provided: + * - **Label**: Derived from the resource's file name when {@link QuickPickItem.label label} is not provided or is empty. + * - **Description**: Derived from the resource's path when {@link QuickPickItem.description description} is not provided or is empty. + * - **Icon**: Derived from the current file icon theme when {@link QuickPickItem.iconPath iconPath} is set to + * {@link ThemeIcon.File} or {@link ThemeIcon.Folder}. + */ + resourceUri?: Uri; + /** * Optional flag indicating if this item is initially selected. * diff --git a/src/vscode-dts/vscode.proposed.quickPickItemResource.d.ts b/src/vscode-dts/vscode.proposed.quickPickItemResource.d.ts deleted file mode 100644 index dd763a23e568..000000000000 --- a/src/vscode-dts/vscode.proposed.quickPickItemResource.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/59826 - - export interface QuickPickItem { - /** - * A {@link Uri} representing the resource associated with this item. - * - * When set, this property is used to automatically derive several item properties if they are not explicitly provided: - * - **Label**: Derived from the resource's file name when {@link QuickPickItem.label label} is not provided or is empty. - * - **Description**: Derived from the resource's path when {@link QuickPickItem.description description} is not provided or is empty. - * - **Icon**: Derived from the current file icon theme when {@link QuickPickItem.iconPath iconPath} is set to - * {@link ThemeIcon.File} or {@link ThemeIcon.Folder}. - */ - resourceUri?: Uri; - } -} From 1d9de1a9d830654ca24456d437c3672accf0753e Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Tue, 16 Dec 2025 11:11:50 -0800 Subject: [PATCH 12/37] Finalize quickPickPrompt API (#283874) Finalize quickPickPrompt API proposal --- .../common/extensionsApiProposals.ts | 3 --- .../workbench/api/common/extHostQuickOpen.ts | 5 ---- src/vscode-dts/vscode.d.ts | 14 ++++++++++ .../vscode.proposed.quickPickPrompt.d.ts | 27 ------------------- 4 files changed, 14 insertions(+), 35 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.quickPickPrompt.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 00ac167445e3..65bcacd26e62 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -337,9 +337,6 @@ const _allApiProposals = { quickPickItemTooltip: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', }, - quickPickPrompt: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickPrompt.d.ts', - }, quickPickSortByLabel: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', }, diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 3cff3d072d36..a25615a41085 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -66,10 +66,6 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx const instance = ++this._instances; - if (options?.prompt) { - checkProposedApiEnabled(extension, 'quickPickPrompt'); - } - const quickPickWidget = proxy.$show(instance, { title: options?.title, placeHolder: options?.placeHolder, @@ -648,7 +644,6 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } set prompt(prompt: string | undefined) { - checkProposedApiEnabled(this._extension, 'quickPickPrompt'); this._prompt = prompt; this.update({ prompt }); } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index d4eb6321a9d5..66242a0366f6 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -2011,6 +2011,13 @@ declare module 'vscode' { */ placeHolder?: string; + /** + * Optional text that provides instructions or context to the user. + * + * The prompt is displayed below the input box and above the list of items. + */ + prompt?: string; + /** * Set to `true` to keep the picker open when focus moves to another part of the editor or to another window. * This setting is ignored on iPad and is always `false`. @@ -13135,6 +13142,13 @@ declare module 'vscode' { */ placeholder: string | undefined; + /** + * Optional text that provides instructions or context to the user. + * + * The prompt is displayed below the input box and above the list of items. + */ + prompt: string | undefined; + /** * An event signaling when the value of the filter text has changed. */ diff --git a/src/vscode-dts/vscode.proposed.quickPickPrompt.d.ts b/src/vscode-dts/vscode.proposed.quickPickPrompt.d.ts deleted file mode 100644 index 2c37c8f71e06..000000000000 --- a/src/vscode-dts/vscode.proposed.quickPickPrompt.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/78335 - - export interface QuickPick extends QuickInput { - /** - * Optional text that provides instructions or context to the user. - * - * The prompt is displayed below the input box and above the list of items. - */ - prompt: string | undefined; - } - - export interface QuickPickOptions { - /** - * Optional text that provides instructions or context to the user. - * - * The prompt is displayed below the input box and above the list of items. - */ - prompt?: string; - } -} From aeb743289ba8f2bf50b1cc7e9ac14254422a96ea Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:12:26 -0800 Subject: [PATCH 13/37] adding title for fetched resources content display (#283742) * adding title for fetched resources content display * adding title for fetched resources content display --- .../chatToolInputOutputContentPart.ts | 1 + .../chatToolOutputContentSubPart.ts | 20 +++++++++++++++++-- .../chatToolPostExecuteConfirmationPart.ts | 1 + .../chat/common/languageModelToolsService.ts | 2 ++ .../electron-browser/tools/fetchPageTool.ts | 17 +++++++++------- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts index 48d97ddffd0f..0d19a3f98d8e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts @@ -33,6 +33,7 @@ export interface IChatCollapsibleIOCodePart { languageId: string; options: ICodeBlockRenderOptions; codeBlockInfo: IChatCodeBlockInfo; + title?: string | IMarkdownString; } export interface IChatCollapsibleIODataPart { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolOutputContentSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolOutputContentSubPart.ts index 59db25467817..9d10743b6133 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolOutputContentSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolOutputContentSubPart.ts @@ -25,6 +25,8 @@ import { IProgressService, ProgressLocation } from '../../../../../platform/prog import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../../files/browser/fileConstants.js'; import { getAttachableImageExtension } from '../../common/chatModel.js'; +import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js'; import { IChatCodeBlockInfo } from '../chat.js'; import { CodeBlockPart, ICodeBlockData } from '../codeBlockPart.js'; @@ -44,22 +46,29 @@ export class ChatToolOutputContentSubPart extends Disposable { private _currentWidth: number = 0; private readonly _editorReferences: IDisposableReference[] = []; public readonly domNode: HTMLElement; - readonly codeblocks: IChatCodeBlockInfo[] = []; constructor( private readonly context: IChatContentPartRenderContext, private readonly parts: ChatCollapsibleIOPart[], - @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IFileService private readonly _fileService: IFileService, + @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, ) { super(); this.domNode = this.createOutputContents(); this._currentWidth = context.currentWidth(); } + private toMdString(value: string | IMarkdownString): MarkdownString { + if (typeof value === 'string') { + return new MarkdownString('').appendText(value); + } + return new MarkdownString(value.value, { isTrusted: value.isTrusted }); + } + private createOutputContents(): HTMLElement { const container = dom.$('div'); @@ -145,6 +154,13 @@ export class ChatToolOutputContentSubPart extends Disposable { } private addCodeBlock(part: IChatCollapsibleIOCodePart, container: HTMLElement) { + if (part.title) { + const title = dom.$('div.chat-confirmation-widget-title'); + const renderedTitle = this._register(this._markdownRendererService.render(this.toMdString(part.title))); + title.appendChild(renderedTitle.element); + container.appendChild(title); + } + const data: ICodeBlockData = { languageId: part.languageId, textModel: Promise.resolve(part.textModel), diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts index 89e0c04609a6..afe760617080 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts @@ -120,6 +120,7 @@ export class ChatToolPostExecuteConfirmationPart extends AbstractToolConfirmatio parts.push({ kind: 'code', + title: part.title, textModel: model, languageId: model.getLanguageId(), options: { diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index 92368fb0020c..7ce9ecfea10c 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -240,6 +240,7 @@ export interface IToolResultTextPart { kind: 'text'; value: string; audience?: LanguageModelPartAudience[]; + title?: string; } export interface IToolResultDataPart { @@ -249,6 +250,7 @@ export interface IToolResultDataPart { data: VSBuffer; }; audience?: LanguageModelPartAudience[]; + title?: string; } export interface IToolConfirmationMessages { diff --git a/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts index 72763be5bdb2..d4291864b8c8 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts @@ -144,7 +144,7 @@ export class FetchWebPageTool implements IToolImpl { const actuallyValidUris = [...webUris.values(), ...successfulFileUris]; return { - content: this._getPromptPartsForResults(results), + content: this._getPromptPartsForResults(urls, results), toolResultDetails: actuallyValidUris, confirmResults, }; @@ -278,28 +278,31 @@ export class FetchWebPageTool implements IToolImpl { return { webUris, fileUris, invalidUris }; } - private _getPromptPartsForResults(results: ResultType[]): (IToolResultTextPart | IToolResultDataPart)[] { - return results.map(value => { + private _getPromptPartsForResults(urls: string[], results: ResultType[]): (IToolResultTextPart | IToolResultDataPart)[] { + return results.map((value, i) => { + const title = results.length > 1 ? localize('fetchWebPage.fetchedFrom', 'Fetched from {0}', urls[i]) : undefined; if (!value) { return { kind: 'text', + title, value: localize('fetchWebPage.invalidUrl', 'Invalid URL') }; } else if (typeof value === 'string') { return { kind: 'text', + title, value: value }; } else if (value.type === 'tooldata') { - return value.value; + return { ...value.value, title }; } else if (value.type === 'extracted') { switch (value.value.status) { case 'ok': - return { kind: 'text', value: value.value.result }; + return { kind: 'text', title, value: value.value.result }; case 'redirect': - return { kind: 'text', value: `The webpage has redirected to "${value.value.toURI.toString(true)}". Use the ${InternalFetchWebPageToolId} again to get its contents.` }; + return { kind: 'text', title, value: `The webpage has redirected to "${value.value.toURI.toString(true)}". Use the ${InternalFetchWebPageToolId} again to get its contents.` }; case 'error': - return { kind: 'text', value: `An error occurred retrieving the fetch result: ${value.value.error}` }; + return { kind: 'text', title, value: `An error occurred retrieving the fetch result: ${value.value.error}` }; default: assertNever(value.value); } From 2b49f34380051ffd69b762d0a73230b4a2fb6cde Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:26:15 -0600 Subject: [PATCH 14/37] Fix "Create New Terminal in Editor Area" to respect focused window (#283102) --- src/vs/workbench/contrib/terminal/browser/terminalActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 8f65e84d177b..510344052c3b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -47,7 +47,7 @@ import { IConfigurationResolverService } from '../../../services/configurationRe import { ConfigurationResolverExpression } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; import { editorGroupToColumn } from '../../../services/editor/common/editorGroupColumn.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; -import { AUX_WINDOW_GROUP, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { ACTIVE_GROUP, AUX_WINDOW_GROUP, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; @@ -319,7 +319,7 @@ export function registerTerminalActions() { function isCreateTerminalOptions(obj: unknown): obj is ICreateTerminalOptions { return isObject(obj) && 'location' in obj; } - const options = isCreateTerminalOptions(args) ? args : { location: TerminalLocation.Editor }; + const options = isCreateTerminalOptions(args) ? args : { location: { viewColumn: ACTIVE_GROUP } }; const instance = await c.service.createTerminal(options); await instance.focusWhenReady(); } From 4290065b2988f1f3ea42d0bc51f6e732f7a6dd01 Mon Sep 17 00:00:00 2001 From: Aaron Munger <2019016+amunger@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:29:08 -0800 Subject: [PATCH 15/37] notebook tests still failing too often (#283882) still failing too often --- test/smoke/src/areas/notebook/notebook.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/smoke/src/areas/notebook/notebook.test.ts b/test/smoke/src/areas/notebook/notebook.test.ts index 97f0b634e1b5..a0b81837266d 100644 --- a/test/smoke/src/areas/notebook/notebook.test.ts +++ b/test/smoke/src/areas/notebook/notebook.test.ts @@ -44,7 +44,7 @@ export function setup(logger: Logger) { }); }); - it('inserts/edits code cell', async function () { + it.skip('inserts/edits code cell', async function () { const app = this.app as Application; await app.workbench.notebook.openNotebook(); await app.workbench.notebook.focusNextCell(); @@ -64,7 +64,7 @@ export function setup(logger: Logger) { await app.workbench.notebook.waitForMarkdownContents('', ''); }); - it('moves focus as it inserts/deletes a cell', async function () { + it.skip('moves focus as it inserts/deletes a cell', async function () { const app = this.app as Application; await app.workbench.notebook.openNotebook(); await app.workbench.notebook.focusFirstCell(); @@ -78,7 +78,7 @@ export function setup(logger: Logger) { await app.workbench.notebook.waitForActiveCellEditorContents('# added cell'); }); - it('moves focus in and out of output', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/139270 + it.skip('moves focus in and out of output', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/139270 const app = this.app as Application; await app.workbench.notebook.openNotebook(); // first cell is a code cell that already has output From f9c4d40c2fbc488f77fb6ca8570675254c651358 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 16 Dec 2025 13:41:48 -0600 Subject: [PATCH 16/37] Update msCompile matcher to handle more cases (#283885) * fixes #274750 * fixes #167454 --- .../contrib/tasks/common/problemMatcher.ts | 11 +- .../tasks/test/common/problemMatcher.test.ts | 130 ++++++++++++++++++ 2 files changed, 137 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts index 4e83d5b40c90..a758a3e4e865 100644 --- a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts @@ -502,6 +502,9 @@ class SingleLineMatcher extends AbstractLineMatcher { const matches = this.pattern.regexp.exec(lines[start]); if (matches) { this.fillProblemData(data, this.pattern, matches); + if (data.kind === ProblemLocationKind.Location && !data.location && !data.line && data.file) { + data.kind = ProblemLocationKind.File; + } const match = this.getMarkerMatch(data); if (match) { return { match: match, continue: false }; @@ -1501,13 +1504,13 @@ class ProblemPatternRegistryImpl implements IProblemPatternRegistry { private fillDefaults(): void { this.add('msCompile', { - regexp: /^(?:\s*\d+>)?(\S.*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\)\s*:\s+((?:fatal +)?error|warning|info)\s+(\w+\d+)\s*:\s*(.*)$/, + regexp: /^\s*(?:\s*\d+>)?(\S.*?)(?:\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\))?\s*:\s+(?:(\S+)\s+)?((?:fatal +)?error|warning|info)\s+(\w+\d+)?\s*:\s*(.*)$/, kind: ProblemLocationKind.Location, file: 1, location: 2, - severity: 3, - code: 4, - message: 5 + severity: 4, + code: 5, + message: 6 }); this.add('gulp-tsc', { regexp: /^([^\s].*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\):\s+(\d+)\s+(.*)$/, diff --git a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts index 0b14df78ffe8..2248500652bd 100644 --- a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts @@ -6,6 +6,7 @@ import * as matchers from '../../common/problemMatcher.js'; import assert from 'assert'; import { ValidationState, IProblemReporter, ValidationStatus } from '../../../../../base/common/parsers.js'; +import { MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; class ProblemReporter implements IProblemReporter { @@ -266,3 +267,132 @@ suite('ProblemPatternParser', () => { }); }); }); + +suite('ProblemPatternRegistry - msCompile', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('matches lines with leading whitespace', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = ' /workspace/app.cs(5,10): error CS1001: Sample message'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'CS1001'); + assert.strictEqual(marker.message, 'Sample message'); + }); + + test('matches lines without diagnostic code', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = '/workspace/app.cs(3,7): warning : Message without code'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, undefined); + assert.strictEqual(marker.message, 'Message without code'); + }); + + test('matches lines without location information', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = 'Main.cs: warning CS0168: The variable \'x\' is declared but never used'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'CS0168'); + assert.strictEqual(marker.message, 'The variable \'x\' is declared but never used'); + assert.strictEqual(marker.severity, MarkerSeverity.Warning); + }); + + test('matches lines with build prefixes and fatal errors', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = ' 1>c:/workspace/app.cs(12): fatal error C1002: Fatal diagnostics'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'C1002'); + assert.strictEqual(marker.message, 'Fatal diagnostics'); + assert.strictEqual(marker.severity, MarkerSeverity.Error); + }); + + test('matches info diagnostics with codes', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = '2>/workspace/app.cs(20,5): info INF1001: Informational diagnostics'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'INF1001'); + assert.strictEqual(marker.message, 'Informational diagnostics'); + assert.strictEqual(marker.severity, MarkerSeverity.Info); + }); + + test('matches lines with subcategory prefixes', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = 'Main.cs(17,20): subcategory warning CS0168: The variable \'x\' is declared but never used'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'CS0168'); + assert.strictEqual(marker.message, 'The variable \'x\' is declared but never used'); + assert.strictEqual(marker.severity, MarkerSeverity.Warning); + }); + + test('matches complex diagnostics with all qualifiers', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = ' 12>c:/workspace/Main.cs(42,7,43,2): subcategory fatal error CS9999: Complex diagnostics'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'CS9999'); + assert.strictEqual(marker.message, 'Complex diagnostics'); + assert.strictEqual(marker.severity, MarkerSeverity.Error); + assert.strictEqual(marker.startLineNumber, 42); + assert.strictEqual(marker.startColumn, 7); + assert.strictEqual(marker.endLineNumber, 43); + assert.strictEqual(marker.endColumn, 2); + }); + + test('ignores diagnostics without origin', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = 'warning: The variable \'x\' is declared but never used'; + const result = matcher.handle([line]); + assert.strictEqual(result.match, null); + }); +}); From 7763ee5683f9fc8572a6d9bbbcec5267eeb72793 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:52:26 -0800 Subject: [PATCH 17/37] check the end part of `legacyToolReferenceFullNames` when evaluating `EligibleForAutoApproval` (#283887) * check the end part of legacyToolReferenceFullNames when evaluating EligibleForAutoApproval * improve * tests --- .../chat/browser/languageModelToolsService.ts | 9 +- .../browser/languageModelToolsService.test.ts | 174 +++++++++++++++++- 2 files changed, 174 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 1a8032aa2316..5467f4bc1e80 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -613,13 +613,20 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Back compat with legacy names if (toolData.legacyToolReferenceFullNames) { for (const legacyName of toolData.legacyToolReferenceFullNames) { + // Check if the full legacy name is in the config if (Object.prototype.hasOwnProperty.call(eligibilityConfig, legacyName)) { return eligibilityConfig[legacyName]; } + // Some tools may be both renamed and namespaced from a toolset, eg: xxx/yyy -> yyy + if (legacyName.includes('/')) { + const trimmedLegacyName = legacyName.split('/').pop(); + if (trimmedLegacyName && Object.prototype.hasOwnProperty.call(eligibilityConfig, trimmedLegacyName)) { + return eligibilityConfig[trimmedLegacyName]; + } + } } } } - // Default true return true; } diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 0fcdd88041d1..6faf6b6b7db9 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -1388,7 +1388,7 @@ suite('LanguageModelToolsService', () => { test('eligibleForAutoApproval setting controls tool eligibility', async () => { // Test the new eligibleForAutoApproval setting const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.eligibleForAutoApproval', { + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { 'eligibleToolRef': true, 'ineligibleToolRef': false }); @@ -2166,7 +2166,7 @@ suite('LanguageModelToolsService', () => { 'toolA': true, 'toolB': false }; - testConfigService.setUserConfiguration('chat.tools.eligibleForAutoApproval', policyValue); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, policyValue); const instaService = workbenchInstantiationService({ contextKeyService: () => store.add(new ContextKeyService(testConfigService)), @@ -2223,7 +2223,7 @@ suite('LanguageModelToolsService', () => { test('eligibleForAutoApproval with legacy tool reference names - eligible', async () => { // Test backwards compatibility: configuring a legacy name as eligible should work const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.eligibleForAutoApproval', { + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { 'oldToolName': true // Using legacy name }); @@ -2259,7 +2259,7 @@ suite('LanguageModelToolsService', () => { test('eligibleForAutoApproval with legacy tool reference names - ineligible', async () => { // Test backwards compatibility: configuring a legacy name as ineligible should work const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.eligibleForAutoApproval', { + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { 'deprecatedToolName': false // Using legacy name }); @@ -2302,7 +2302,7 @@ suite('LanguageModelToolsService', () => { test('eligibleForAutoApproval with multiple legacy names', async () => { // Test that any of the legacy names can be used in the configuration const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.eligibleForAutoApproval', { + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { 'secondLegacyName': true // Using the second legacy name }); @@ -2338,7 +2338,7 @@ suite('LanguageModelToolsService', () => { test('eligibleForAutoApproval current name takes precedence over legacy names', async () => { // Test forward compatibility: current name in config should take precedence const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.eligibleForAutoApproval', { + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { 'currentName': false, // Current name says ineligible 'oldName': true // Legacy name says eligible }); @@ -2381,7 +2381,7 @@ suite('LanguageModelToolsService', () => { test('eligibleForAutoApproval with legacy full reference names from toolsets', async () => { // Test legacy names that include toolset prefixes (e.g., 'oldToolSet/oldToolName') const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.eligibleForAutoApproval', { + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { 'oldToolSet/oldToolName': false // Legacy full reference name from old toolset }); @@ -2424,7 +2424,7 @@ suite('LanguageModelToolsService', () => { test('eligibleForAutoApproval mixed current and legacy names', async () => { // Test realistic migration scenario with mixed current and legacy names const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.eligibleForAutoApproval', { + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { 'modernTool': true, // Current name 'legacyToolOld': false, // Legacy name 'unchangedTool': true // Tool that never changed @@ -2498,6 +2498,164 @@ suite('LanguageModelToolsService', () => { assert.strictEqual(result3.content[0].value, 'unchanged executed'); }); + test('eligibleForAutoApproval with namespaced legacy names - full tool name eligible', async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'gitTools/gitCommit': true + }); + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + const tool = registerToolForTest(testService, store, 'gitCommitTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'commit executed' }] }) + }, { + toolReferenceName: 'commit', + legacyToolReferenceFullNames: ['gitTools/gitCommit'] + }); + + const sessionId = 'test-extension-prefix'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool should be eligible via legacy extension-prefixed name + const result = await testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + + const published = await waitForPublishedInvocation(capture); + assert.strictEqual(published, undefined, 'tool should not require confirmation when legacy trimmed name is eligible'); + assert.strictEqual(result.content[0].value, 'commit executed'); + }); + + test('eligibleForAutoApproval with namespaced and renamed toolname - just last segment eligible', async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'gitCommit': true + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool that was previously namespaced under extension but is now internal + const tool = registerToolForTest(testService, store, 'gitCommitTool2', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'commit executed' }] }) + }, { + toolReferenceName: 'commit', + legacyToolReferenceFullNames: ['gitTools/gitCommit'] + }); + + const sessionId = 'test-renamed-prefix'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool should be eligible via legacy extension-prefixed name + const result = await testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + + const published = await waitForPublishedInvocation(capture); + assert.strictEqual(published, undefined, 'tool should not require confirmation when legacy trimmed name is eligible'); + assert.strictEqual(result.content[0].value, 'commit executed'); + }); + + test('eligibleForAutoApproval with namespaced legacy names - full tool name ineligible', async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'gitTools/gitCommit': false + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool that was previously namespaced under extension but is now internal + const tool = registerToolForTest(testService, store, 'gitCommitTool3', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'commit blocked' }] }) + }, { + toolReferenceName: 'commit', + legacyToolReferenceFullNames: ['something/random', 'gitTools/bar', 'gitTools/gitCommit'] + }); + + const sessionId = 'test-extension-prefix-blocked'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1', capture }); + + // Tool should be ineligible via legacy extension-prefixed name + const promise = testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'tool should require confirmation when legacy full name is ineligible'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(result.content[0].value, 'commit blocked'); + }); + + test('eligibleForAutoApproval with namespaced and renamed toolname - just last segment ineligible', async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'gitCommit': false + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool that was previously namespaced under extension but is now internal + const tool = registerToolForTest(testService, store, 'gitCommitTool4', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'commit blocked' }] }) + }, { + toolReferenceName: 'commit', + legacyToolReferenceFullNames: ['something/random', 'gitTools/bar', 'gitTools/gitCommit'] + }); + + const sessionId = 'test-renamed-prefix-blocked'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1', capture }); + + // Tool should be ineligible via trimmed legacy name + const promise = testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'tool should require confirmation when legacy trimmed name is ineligible'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(result.content[0].value, 'commit blocked'); + }); }); From 4d96ec5563bbd2c1c4b35f890590e21d87a19c0a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 16 Dec 2025 20:52:55 +0100 Subject: [PATCH 18/37] agent sessions - code polish (#283888) --- src/vs/base/common/marshallingIds.ts | 2 +- .../api/common/extHostChatSessions.ts | 2 +- .../browser/agentSessions/agentSessions.ts | 17 +------ .../agentSessions/agentSessionsActions.ts | 13 +++--- .../agentSessions/agentSessionsControl.ts | 6 +-- .../agentSessions/agentSessionsFilter.ts | 16 +++---- .../agentSessions/agentSessionsModel.ts | 45 ++++++++++++------- .../agentSessions/agentSessionsViewer.ts | 31 +++++++------ 8 files changed, 63 insertions(+), 69 deletions(-) diff --git a/src/vs/base/common/marshallingIds.ts b/src/vs/base/common/marshallingIds.ts index 4400c6246f3d..730fbd615330 100644 --- a/src/vs/base/common/marshallingIds.ts +++ b/src/vs/base/common/marshallingIds.ts @@ -28,6 +28,6 @@ export const enum MarshalledId { LanguageModelThinkingPart, LanguageModelPromptTsxPart, LanguageModelDataPart, - ChatSessionContext, + AgentSessionContext, ChatResponsePullRequestPart, } diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 1ee8852ffc4f..833bf30e20a7 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -96,7 +96,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio commands.registerArgumentProcessor({ processArgument: (arg) => { - if (arg && arg.$mid === MarshalledId.ChatSessionContext) { + if (arg && arg.$mid === MarshalledId.AgentSessionContext) { const id = arg.session.resource || arg.sessionId; const sessionContent = this._sessionItems.get(id); if (sessionContent) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index bcd50fa0f888..e038139127f9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -7,9 +7,8 @@ import { localize } from '../../../../../nls.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { URI } from '../../../../../base/common/uri.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { IChatSessionItem, localChatSessionType } from '../../common/chatSessionsService.js'; +import { localChatSessionType } from '../../common/chatSessionsService.js'; import { foreground, listActiveSelectionForeground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; -import { MarshalledId } from '../../../../../base/common/marshallingIds.js'; export enum AgentSessionProviders { Local = localChatSessionType, @@ -72,17 +71,3 @@ export const agentSessionSelectedUnfocusedBadgeBorder = registerColor( { dark: transparent(foreground, 0.3), light: transparent(foreground, 0.3), hcDark: foreground, hcLight: foreground }, localize('agentSessionSelectedUnfocusedBadgeBorder', "Border color for the badges in selected agent session items when the view is unfocused.") ); - -export interface IMarshalledChatSessionContext { - readonly $mid: MarshalledId.ChatSessionContext; - readonly session: IChatSessionItem; -} - -export function isMarshalledChatSessionContext(thing: unknown): thing is IMarshalledChatSessionContext { - if (typeof thing === 'object' && thing !== null) { - const candidate = thing as IMarshalledChatSessionContext; - return candidate.$mid === MarshalledId.ChatSessionContext && typeof candidate.session === 'object' && candidate.session !== null; - } - - return false; -} diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index ab89cf0c0e0c..847bb188875e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { localize, localize2 } from '../../../../../nls.js'; -import { AgentSessionSection, IAgentSession, IAgentSessionSection, isAgentSessionSection } from './agentSessionsModel.js'; +import { AgentSessionSection, IAgentSession, IAgentSessionSection, IMarshalledAgentSessionContext, isAgentSessionSection, isMarshalledAgentSessionContext } from './agentSessionsModel.js'; import { Action2, MenuId, MenuRegistry } from '../../../../../platform/actions/common/actions.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { AgentSessionsViewerOrientation, IAgentSessionsControl, IMarshalledChatSessionContext, isMarshalledChatSessionContext } from './agentSessions.js'; +import { AgentSessionProviders, AgentSessionsViewerOrientation, IAgentSessionsControl } from './agentSessions.js'; import { IChatService } from '../../common/chatService.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatEditorOptions } from '../chatEditor.js'; @@ -31,7 +31,6 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { AgentSessionsPicker } from './agentSessionsPicker.js'; import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; -import { localChatSessionType } from '../../common/chatSessionsService.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; @@ -264,12 +263,12 @@ export class ArchiveAgentSessionSectionAction extends Action2 { abstract class BaseAgentSessionAction extends Action2 { - run(accessor: ServicesAccessor, context?: IAgentSession | IMarshalledChatSessionContext): void { + run(accessor: ServicesAccessor, context?: IAgentSession | IMarshalledAgentSessionContext): void { const agentSessionsService = accessor.get(IAgentSessionsService); const viewsService = accessor.get(IViewsService); let session: IAgentSession | undefined; - if (isMarshalledChatSessionContext(context)) { + if (isMarshalledAgentSessionContext(context)) { session = agentSessionsService.getSession(context.session.resource); } else { session = context; @@ -433,14 +432,14 @@ export class RenameAgentSessionAction extends BaseAgentSessionAction { weight: KeybindingWeight.WorkbenchContrib + 1, when: ContextKeyExpr.and( ChatContextKeys.agentSessionsViewerFocused, - ChatContextKeys.agentSessionType.isEqualTo(localChatSessionType) + ChatContextKeys.agentSessionType.isEqualTo(AgentSessionProviders.Local) ), }, menu: { id: MenuId.AgentSessionsContext, group: 'edit', order: 3, - when: ChatContextKeys.agentSessionType.isEqualTo(localChatSessionType) + when: ChatContextKeys.agentSessionType.isEqualTo(AgentSessionProviders.Local) } }); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index b2c50219fec2..46a6fcd29b11 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -9,7 +9,7 @@ import { IContextMenuService } from '../../../../../platform/contextview/browser import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js'; import { $, append, EventHelper } from '../../../../../base/browser/dom.js'; -import { IAgentSession, IAgentSessionsModel, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; +import { IAgentSession, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js'; import { FuzzyScore } from '../../../../../base/common/filters.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; @@ -26,7 +26,7 @@ import { IAgentSessionsService } from './agentSessionsService.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IListStyles } from '../../../../../base/browser/ui/list/listWidget.js'; import { IStyleOverride } from '../../../../../platform/theme/browser/defaultStyles.js'; -import { IAgentSessionsControl, IMarshalledChatSessionContext } from './agentSessions.js'; +import { IAgentSessionsControl } from './agentSessions.js'; import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; import { URI } from '../../../../../base/common/uri.js'; import { openSession } from './agentSessionsOpener.js'; @@ -177,7 +177,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo contextOverlay.push([ChatContextKeys.agentSessionType.key, element.providerType]); const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay)); - const marshalledSession: IMarshalledChatSessionContext = { session: element, $mid: MarshalledId.ChatSessionContext }; + const marshalledSession: IMarshalledAgentSessionContext = { session: element, $mid: MarshalledId.AgentSessionContext }; this.contextMenuService.showContextMenu({ getActions: () => Separator.join(...menu.getActions({ arg: marshalledSession, shouldForwardArgs: true }).map(([, actions]) => actions)), getAnchor: () => anchor, diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index 36803a6fc9a0..5066920f3653 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -10,9 +10,9 @@ import { localize } from '../../../../../nls.js'; import { registerAction2, Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { ChatSessionStatus, IChatSessionsService } from '../../common/chatSessionsService.js'; +import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProviderName } from './agentSessions.js'; -import { IAgentSession } from './agentSessionsModel.js'; +import { AgentSessionStatus, IAgentSession } from './agentSessionsModel.js'; import { IAgentSessionsFilter } from './agentSessionsViewer.js'; export interface IAgentSessionsFilterOptions extends Partial { @@ -29,7 +29,7 @@ export interface IAgentSessionsFilterOptions extends Partial; @@ -34,14 +38,14 @@ export interface IAgentSessionsModel { resolve(provider: string | string[] | undefined): Promise; } -interface IAgentSessionData { +interface IAgentSessionData extends Omit { readonly providerType: string; readonly providerLabel: string; readonly resource: URI; - readonly status: ChatSessionStatus; + readonly status: AgentSessionStatus; readonly tooltip?: string | IMarkdownString; @@ -50,19 +54,12 @@ interface IAgentSessionData { readonly badge?: string | IMarkdownString; readonly icon: ThemeIcon; - readonly timing: { - readonly startTime: number; - readonly endTime?: number; - + readonly timing: IChatSessionItem['timing'] & { readonly inProgressTime?: number; readonly finishedOrFailedTime?: number; }; - readonly changes?: readonly IChatSessionFileChange[] | { - readonly files: number; - readonly insertions: number; - readonly deletions: number; - }; + readonly changes?: IChatSessionItem['changes']; } /** @@ -125,7 +122,7 @@ interface IInternalAgentSessionData extends IAgentSessionData { interface IInternalAgentSession extends IAgentSession, IInternalAgentSessionData { } export function isLocalAgentSessionItem(session: IAgentSession): boolean { - return session.providerType === localChatSessionType; + return session.providerType === AgentSessionProviders.Local; } export function isAgentSession(obj: unknown): obj is IAgentSession { @@ -166,6 +163,20 @@ export function isAgentSessionSection(obj: IAgentSessionsModel | IAgentSession | return typeof candidate.section === 'string' && Array.isArray(candidate.sessions); } +export interface IMarshalledAgentSessionContext { + readonly $mid: MarshalledId.AgentSessionContext; + readonly session: IAgentSession; +} + +export function isMarshalledAgentSessionContext(thing: unknown): thing is IMarshalledAgentSessionContext { + if (typeof thing === 'object' && thing !== null) { + const candidate = thing as IMarshalledAgentSessionContext; + return candidate.$mid === MarshalledId.AgentSessionContext && typeof candidate.session === 'object' && candidate.session !== null; + } + + return false; +} + //#endregion export class AgentSessionsModel extends Disposable implements IAgentSessionsModel { @@ -186,7 +197,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode private readonly providersToResolve = new Set(); private readonly mapSessionToState = new ResourceMap<{ - status: ChatSessionStatus; + status: AgentSessionStatus; inProgressTime?: number; finishedOrFailedTime?: number; @@ -315,7 +326,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode // yet: we only track the time when a transition changes because then we can say with // confidence that the time is correct by assuming `Date.now()`. A better approach would // be to get all this information directly from the session. - const status = session.status ?? ChatSessionStatus.Completed; + const status = session.status ?? AgentSessionStatus.Completed; const state = this.mapSessionToState.get(session.resource); let inProgressTime = state?.inProgressTime; let finishedOrFailedTime = state?.finishedOrFailedTime; @@ -458,14 +469,14 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode //#region Sessions Cache -interface ISerializedAgentSession { +interface ISerializedAgentSession extends Omit { readonly providerType: string; readonly providerLabel: string; readonly resource: UriComponents; - readonly status: ChatSessionStatus; + readonly status: AgentSessionStatus; readonly tooltip?: string | IMarkdownString; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 6f667dde2ace..dc74bf4f6a0a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -13,7 +13,7 @@ import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compres import { ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js'; import { ITreeNode, ITreeElementRenderDetails, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { AgentSessionSection, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel } from './agentSessionsModel.js'; +import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isSessionInProgressStatus } from './agentSessionsModel.js'; import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; @@ -27,7 +27,6 @@ import { ListViewTargetSector } from '../../../../../base/browser/ui/list/listVi import { coalesce } from '../../../../../base/common/arrays.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { fillEditorsDragData } from '../../../../browser/dnd.js'; -import { ChatSessionStatus, isSessionInProgressStatus } from '../../common/chatSessionsService.js'; import { HoverStyle } from '../../../../../base/browser/ui/hover/hover.js'; import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; @@ -247,15 +246,15 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer { compare(sessionA: IAgentSession, sessionB: IAgentSession): number { // Input Needed - const aNeedsInput = sessionA.status === ChatSessionStatus.NeedsInput; - const bNeedsInput = sessionB.status === ChatSessionStatus.NeedsInput; + const aNeedsInput = sessionA.status === AgentSessionStatus.NeedsInput; + const bNeedsInput = sessionB.status === AgentSessionStatus.NeedsInput; if (aNeedsInput && !bNeedsInput) { return -1; // a (needs input) comes before b (other) @@ -697,8 +696,8 @@ export class AgentSessionsSorter implements ITreeSorter { } // In Progress - const aInProgress = sessionA.status === ChatSessionStatus.InProgress; - const bInProgress = sessionB.status === ChatSessionStatus.InProgress; + const aInProgress = sessionA.status === AgentSessionStatus.InProgress; + const bInProgress = sessionB.status === AgentSessionStatus.InProgress; if (aInProgress && !bInProgress) { return -1; // a (in-progress) comes before b (finished) From 4d44b4e2fd6cf3bdbdc52f63d0b358e4b3fca802 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 16 Dec 2025 21:02:22 +0100 Subject: [PATCH 19/37] agent sessions - adopt worktree icon (#283890) --- .../contrib/chat/browser/agentSessions/agentSessions.ts | 2 +- .../contrib/chat/test/browser/agentSessionViewModel.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index e038139127f9..eb0f513ab4fa 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -32,7 +32,7 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th case AgentSessionProviders.Local: return Codicon.vm; case AgentSessionProviders.Background: - return Codicon.collection; + return Codicon.worktree; case AgentSessionProviders.Cloud: return Codicon.cloud; } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts index 8183ac4cc3c4..3cee9c07c2ee 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts @@ -1817,7 +1817,7 @@ suite('Agent Sessions', () => { test('should return correct icon for Background provider', () => { const icon = getAgentSessionProviderIcon(AgentSessionProviders.Background); - assert.strictEqual(icon.id, Codicon.collection.id); + assert.strictEqual(icon.id, Codicon.worktree.id); }); test('should return correct icon for Cloud provider', () => { @@ -1874,7 +1874,7 @@ suite('Agent Sessions', () => { const session = viewModel.sessions[0]; assert.strictEqual(session.providerType, AgentSessionProviders.Background); - assert.strictEqual(session.icon.id, Codicon.collection.id); + assert.strictEqual(session.icon.id, Codicon.worktree.id); assert.strictEqual(session.providerLabel, getAgentSessionProviderName(AgentSessionProviders.Background)); }); }); From 23ab72a3acfcb9e3dbbedafb67f1a45980fc9761 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 16 Dec 2025 14:21:11 -0600 Subject: [PATCH 20/37] don't play `Clear` signal when a new chat is created (#283896) fixes #281555 --- .../contrib/chat/browser/actions/chatNewActions.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 88003d5580d3..4e52846f8789 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -7,7 +7,6 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize2 } from '../../../../../nls.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -68,10 +67,6 @@ export function registerNewChatActions() { }); } async run(accessor: ServicesAccessor, ...args: unknown[]) { - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - - accessibilitySignalService.playSignal(AccessibilitySignal.clear); - await clearChatEditor(accessor); } }); @@ -125,7 +120,6 @@ export function registerNewChatActions() { return; } - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); const dialogService = accessor.get(IDialogService); const model = widget.viewModel?.model; @@ -133,8 +127,6 @@ export function registerNewChatActions() { return; } - accessibilitySignalService.playSignal(AccessibilitySignal.clear); - await editingSession?.stop(); await widget.clear(); widget.attachmentModel.clear(true); From 4dcdc3c6726da250d6bd65269cdbbfb761134d38 Mon Sep 17 00:00:00 2001 From: "Erez A. Korn" Date: Tue, 16 Dec 2025 13:06:58 -0800 Subject: [PATCH 21/37] Chat: hide Apply in Editor from Command Palette (#283486) Fixes #253310 --- .../contrib/chat/browser/actions/chatCodeblockActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 30d91d45c3f7..e7e62d0da85f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -278,7 +278,7 @@ export function registerChatCodeBlockActions() { id: APPLY_IN_EDITOR_ID, title: localize2('interactive.applyInEditor.label', "Apply in Editor"), precondition: ChatContextKeys.enabled, - f1: true, + f1: false, category: CHAT_CATEGORY, icon: Codicon.gitPullRequestGoToChanges, From 0bd621a4179421a2ac6a7d100e20f85b0f1e999e Mon Sep 17 00:00:00 2001 From: przpl <9073573+przpl@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:07:57 +0100 Subject: [PATCH 22/37] fix(runSubagent): collect computed attachments (#283750) * fix(runSubagent): collect computed attachments * style(runSubagent): format code for consistency --- .../chat/common/tools/runSubagentTool.ts | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts index 1ab477c97ec4..67ffed45d571 100644 --- a/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts @@ -12,11 +12,13 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IChatAgentRequest, IChatAgentService } from '../chatAgents.js'; +import { IChatAgentRequest, IChatAgentService, UserSelectedTools } from '../chatAgents.js'; import { ChatModel, IChatRequestModeInstructions } from '../chatModel.js'; import { IChatModeService } from '../chatModes.js'; import { IChatProgress, IChatService } from '../chatService.js'; +import { ChatRequestVariableSet } from '../chatVariableEntries.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../languageModels.js'; import { @@ -31,8 +33,10 @@ import { ToolDataSource, ToolProgress, ToolSet, - VSCodeToolReference + VSCodeToolReference, + IToolAndToolSetEnablementMap } from '../languageModelToolsService.js'; +import { ComputeAutomaticInstructions } from '../promptSyntax/computeAutomaticInstructions.js'; import { ManageTodoListToolToolId } from './manageTodoListTool.js'; import { createToolSimpleTextResult } from './toolHelpers.js'; @@ -65,6 +69,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { @ILogService private readonly logService: ILogService, @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.onDidUpdateToolData = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents)); @@ -214,13 +219,15 @@ export class RunSubagentTool extends Disposable implements IToolImpl { modeTools[ManageTodoListToolToolId] = false; } + const variableSet = await this.collectVariables(modeTools, token); + // Build the agent request const agentRequest: IChatAgentRequest = { sessionResource: invocation.context.sessionResource, requestId: invocation.callId ?? `subagent-${Date.now()}`, agentId: defaultAgent.id, message: args.prompt, - variables: { variables: [] }, + variables: { variables: variableSet.asArray() }, location: ChatAgentLocation.Chat, isSubagent: true, userSelectedModelId: modeModelId, @@ -258,4 +265,26 @@ export class RunSubagentTool extends Disposable implements IToolImpl { invocationMessage: args.description, }; } + + private async collectVariables(modeTools: UserSelectedTools | undefined, token: CancellationToken): Promise { + let enabledTools: IToolAndToolSetEnablementMap | undefined; + + if (modeTools) { + // Convert tool IDs to full reference names + + const enabledToolIds = Object.entries(modeTools).filter(([, enabled]) => enabled).map(([id]) => id); + const tools = enabledToolIds.map(id => this.languageModelToolsService.getTool(id)).filter(tool => !!tool); + + const fullReferenceNames = tools.map(tool => this.languageModelToolsService.getFullReferenceName(tool)); + if (fullReferenceNames.length > 0) { + enabledTools = this.languageModelToolsService.toToolAndToolSetEnablementMap(fullReferenceNames, undefined); + } + } + + const variableSet = new ChatRequestVariableSet(); + const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, enabledTools); + await computer.collect(variableSet, token); + + return variableSet; + } } From 756dd5613a1f932d67ce56fb1b3f1334e1967038 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 16 Dec 2025 15:08:31 -0600 Subject: [PATCH 23/37] refactor, consolidate terminal command mirrors (#283906) --- .../chatTerminalToolProgressPart.ts | 130 +------------ .../browser/chatTerminalCommandMirror.ts | 182 +++++++++++++++--- 2 files changed, 156 insertions(+), 156 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 47736e8667c0..c6240ef2759c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -19,7 +19,7 @@ import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; import '../media/chatTerminalToolProgressPart.css'; import type { ICodeBlockRenderOptions } from '../../codeBlockPart.js'; import { Action, IAction } from '../../../../../../base/common/actions.js'; -import { IChatTerminalToolProgressPart, ITerminalChatService, ITerminalConfigurationService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService, type IDetachedTerminalInstance } from '../../../../terminal/browser/terminal.js'; +import { IChatTerminalToolProgressPart, ITerminalChatService, ITerminalConfigurationService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../../../terminal/browser/terminal.js'; import { Disposable, MutableDisposable, toDisposable, type IDisposable } from '../../../../../../base/common/lifecycle.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; @@ -39,16 +39,13 @@ import { AccessibilityVerbositySettingId } from '../../../../accessibility/brows import { ChatContextKeys } from '../../../common/chatContextKeys.js'; import { EditorPool } from '../chatContentCodePools.js'; import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; -import { DetachedTerminalCommandMirror } from '../../../../terminal/browser/chatTerminalCommandMirror.js'; -import { DetachedProcessInfo } from '../../../../terminal/browser/detachedTerminal.js'; +import { DetachedTerminalCommandMirror, DetachedTerminalSnapshotMirror } from '../../../../terminal/browser/chatTerminalCommandMirror.js'; import { TerminalLocation } from '../../../../../../platform/terminal/common/terminal.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { TerminalContribCommandId } from '../../../../terminal/terminalContribExports.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { isNumber } from '../../../../../../base/common/types.js'; import { removeAnsiEscapeCodes } from '../../../../../../base/common/strings.js'; -import { Color } from '../../../../../../base/common/color.js'; -import { TERMINAL_BACKGROUND_COLOR } from '../../../../terminal/common/terminalColorRegistry.js'; import { PANEL_BACKGROUND } from '../../../../../common/theme.js'; import { editorBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; @@ -1021,129 +1018,6 @@ class ChatTerminalToolOutputSection extends Disposable { } } -class DetachedTerminalSnapshotMirror extends Disposable { - private _detachedTerminal: Promise | undefined; - private _output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined; - private _attachedContainer: HTMLElement | undefined; - private _container: HTMLElement | undefined; - private _dirty = true; - private _lastRenderedLineCount: number | undefined; - - constructor( - output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined, - private readonly _getTheme: () => IChatTerminalToolInvocationData['terminalTheme'] | undefined, - @ITerminalService private readonly _terminalService: ITerminalService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - ) { - super(); - this._output = output; - } - - public setOutput(output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined): void { - this._output = output; - this._dirty = true; - } - - public async attach(container: HTMLElement): Promise { - const terminal = await this._getTerminal(); - container.classList.add('chat-terminal-output-terminal'); - if (this._attachedContainer !== container || container.firstChild === null) { - terminal.attachToElement(container); - this._attachedContainer = container; - } - this._container = container; - this._applyTheme(container); - } - - public async render(): Promise<{ lineCount?: number } | undefined> { - const output = this._output; - if (!output) { - return undefined; - } - if (!this._dirty) { - return { lineCount: this._lastRenderedLineCount ?? output.lineCount }; - } - const terminal = await this._getTerminal(); - terminal.xterm.clearBuffer(); - terminal.xterm.clearSearchDecorations?.(); - if (this._container) { - this._applyTheme(this._container); - } - const text = output.text ?? ''; - const lineCount = output.lineCount ?? this._estimateLineCount(text); - if (!text) { - this._dirty = false; - this._lastRenderedLineCount = lineCount; - return { lineCount: 0 }; - } - await new Promise(resolve => terminal.xterm.write(text, resolve)); - this._dirty = false; - this._lastRenderedLineCount = lineCount; - return { lineCount }; - } - - private _estimateLineCount(text: string): number { - if (!text) { - return 0; - } - const sanitized = text.replace(/\r/g, ''); - const segments = sanitized.split('\n'); - const count = sanitized.endsWith('\n') ? segments.length - 1 : segments.length; - return Math.max(count, 1); - } - - private _applyTheme(container: HTMLElement): void { - const theme = this._getTheme(); - if (!theme) { - container.style.removeProperty('background-color'); - container.style.removeProperty('color'); - return; - } - if (theme.background) { - container.style.backgroundColor = theme.background; - } - if (theme.foreground) { - container.style.color = theme.foreground; - } - } - - private async _getTerminal(): Promise { - if (!this._detachedTerminal) { - this._detachedTerminal = this._createTerminal(); - } - return this._detachedTerminal; - } - - private async _createTerminal(): Promise { - const terminal = await this._terminalService.createDetachedTerminal({ - cols: 80, - rows: 10, - readonly: true, - processInfo: new DetachedProcessInfo({ initialCwd: '' }), - disableOverviewRuler: true, - colorProvider: { - getBackgroundColor: theme => { - const storedBackground = this._getTheme()?.background; - if (storedBackground) { - const color = Color.fromHex(storedBackground); - if (color) { - return color; - } - } - const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); - if (terminalBackground) { - return terminalBackground; - } - // Use editor background when in chat editor, panel background otherwise - const isInEditor = ChatContextKeys.inChatEditor.getValue(this._contextKeyService); - return theme.getColor(isInEditor ? editorBackground : PANEL_BACKGROUND); - } - } - }); - return this._register(terminal); - } -} - export class ToggleChatTerminalOutputAction extends Action implements IAction { private _expanded = false; diff --git a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts index c2c436d3642e..665c070a59ef 100644 --- a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts +++ b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts @@ -15,6 +15,57 @@ import { PANEL_BACKGROUND } from '../../../common/theme.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; +import { Color } from '../../../../base/common/color.js'; +import type { IChatTerminalToolInvocationData } from '../../chat/common/chatService.js'; +import type { IColorTheme } from '../../../../platform/theme/common/themeService.js'; + +function getChatTerminalBackgroundColor(theme: IColorTheme, contextKeyService: IContextKeyService, storedBackground?: string): Color | undefined { + if (storedBackground) { + const color = Color.fromHex(storedBackground); + if (color) { + return color; + } + } + + const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); + if (terminalBackground) { + return terminalBackground; + } + + const isInEditor = ChatContextKeys.inChatEditor.getValue(contextKeyService); + return theme.getColor(isInEditor ? editorBackground : PANEL_BACKGROUND); +} + +/** + * Base class for detached terminal mirrors. + * Handles attaching to containers and managing the detached terminal instance. + */ +abstract class DetachedTerminalMirror extends Disposable { + private _detachedTerminal: Promise | undefined; + private _attachedContainer: HTMLElement | undefined; + + protected _setDetachedTerminal(detachedTerminal: Promise): void { + this._detachedTerminal = detachedTerminal.then(terminal => this._register(terminal)); + } + + protected async _getTerminal(): Promise { + if (!this._detachedTerminal) { + throw new Error('Detached terminal not initialized'); + } + return this._detachedTerminal; + } + + protected async _attachToContainer(container: HTMLElement): Promise { + const terminal = await this._getTerminal(); + container.classList.add('chat-terminal-output-terminal'); + const needsAttach = this._attachedContainer !== container || container.firstChild === null; + if (needsAttach) { + terminal.attachToElement(container); + this._attachedContainer = container; + } + return terminal; + } +} export async function getCommandOutputSnapshot( xtermTerminal: XtermTerminal, @@ -91,10 +142,7 @@ interface IDetachedTerminalCommandMirror { * Mirrors a terminal command's output into a detached terminal instance. * Used in the chat terminal tool progress part to show command output for example. */ -export class DetachedTerminalCommandMirror extends Disposable implements IDetachedTerminalCommandMirror { - private _detachedTerminal: Promise; - private _attachedContainer?: HTMLElement; - +export class DetachedTerminalCommandMirror extends DetachedTerminalMirror implements IDetachedTerminalCommandMirror { constructor( private readonly _xtermTerminal: XtermTerminal, private readonly _command: ITerminalCommand, @@ -102,17 +150,23 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach @IContextKeyService private readonly _contextKeyService: IContextKeyService, ) { super(); - this._detachedTerminal = this._createTerminal(); + const processInfo = this._register(new DetachedProcessInfo({ initialCwd: '' })); + const capabilities = this._register(new TerminalCapabilityStore()); + this._setDetachedTerminal(this._terminalService.createDetachedTerminal({ + cols: this._xtermTerminal.raw!.cols, + rows: 10, + readonly: true, + processInfo, + disableOverviewRuler: true, + capabilities, + colorProvider: { + getBackgroundColor: theme => getChatTerminalBackgroundColor(theme, this._contextKeyService), + }, + })); } async attach(container: HTMLElement): Promise { - const terminal = await this._detachedTerminal; - container.classList.add('chat-terminal-output-terminal'); - const needsAttach = this._attachedContainer !== container || container.firstChild === null; - if (needsAttach) { - terminal.attachToElement(container); - this._attachedContainer = container; - } + await this._attachToContainer(container); } async renderCommand(): Promise<{ lineCount?: number } | undefined> { @@ -123,7 +177,7 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach if (!vt.text) { return { lineCount: 0 }; } - const detached = await this._detachedTerminal; + const detached = await this._getTerminal(); detached.xterm.clearBuffer(); detached.xterm.clearSearchDecorations?.(); await new Promise(resolve => { @@ -131,30 +185,102 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach }); return { lineCount: vt.lineCount }; } +} - private async _createTerminal(): Promise { +/** + * Mirrors a terminal output snapshot into a detached terminal instance. + * Used when the terminal has been disposed of but we still want to show the output. + */ +export class DetachedTerminalSnapshotMirror extends DetachedTerminalMirror { + private _output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined; + private _container: HTMLElement | undefined; + private _dirty = true; + private _lastRenderedLineCount: number | undefined; + + constructor( + output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined, + private readonly _getTheme: () => IChatTerminalToolInvocationData['terminalTheme'] | undefined, + @ITerminalService private readonly _terminalService: ITerminalService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + ) { + super(); + this._output = output; const processInfo = this._register(new DetachedProcessInfo({ initialCwd: '' })); - const capabilities = this._register(new TerminalCapabilityStore()); - const detached = await this._terminalService.createDetachedTerminal({ - cols: this._xtermTerminal.raw!.cols, + this._setDetachedTerminal(this._terminalService.createDetachedTerminal({ + cols: 80, rows: 10, readonly: true, processInfo, disableOverviewRuler: true, - capabilities, colorProvider: { getBackgroundColor: theme => { - const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); - if (terminalBackground) { - return terminalBackground; - } - // Use editor background when in chat editor, panel background otherwise - const isInEditor = ChatContextKeys.inChatEditor.getValue(this._contextKeyService); - return theme.getColor(isInEditor ? editorBackground : PANEL_BACKGROUND); - }, + const storedBackground = this._getTheme()?.background; + return getChatTerminalBackgroundColor(theme, this._contextKeyService, storedBackground); + } } - }); - return this._register(detached); + })); } + public setOutput(output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined): void { + this._output = output; + this._dirty = true; + } + + public async attach(container: HTMLElement): Promise { + await this._attachToContainer(container); + this._container = container; + this._applyTheme(container); + } + + public async render(): Promise<{ lineCount?: number } | undefined> { + const output = this._output; + if (!output) { + return undefined; + } + if (!this._dirty) { + return { lineCount: this._lastRenderedLineCount ?? output.lineCount }; + } + const terminal = await this._getTerminal(); + terminal.xterm.clearBuffer(); + terminal.xterm.clearSearchDecorations?.(); + if (this._container) { + this._applyTheme(this._container); + } + const text = output.text ?? ''; + const lineCount = output.lineCount ?? this._estimateLineCount(text); + if (!text) { + this._dirty = false; + this._lastRenderedLineCount = lineCount; + return { lineCount: 0 }; + } + await new Promise(resolve => terminal.xterm.write(text, resolve)); + this._dirty = false; + this._lastRenderedLineCount = lineCount; + return { lineCount }; + } + + private _estimateLineCount(text: string): number { + if (!text) { + return 0; + } + const sanitized = text.replace(/\r/g, ''); + const segments = sanitized.split('\n'); + const count = sanitized.endsWith('\n') ? segments.length - 1 : segments.length; + return Math.max(count, 1); + } + + private _applyTheme(container: HTMLElement): void { + const theme = this._getTheme(); + if (!theme) { + container.style.removeProperty('background-color'); + container.style.removeProperty('color'); + return; + } + if (theme.background) { + container.style.backgroundColor = theme.background; + } + if (theme.foreground) { + container.style.color = theme.foreground; + } + } } From 3a5f69bb29627d8a9e35adbe4b3f6d5a7d2d6b47 Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:11:01 -0800 Subject: [PATCH 24/37] For post confirm actions do not include url query strings. (#283897) For post confirm actions do not include query strings --- .../contrib/chat/common/chatUrlFetchingConfirmation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatUrlFetchingConfirmation.ts b/src/vs/workbench/contrib/chat/common/chatUrlFetchingConfirmation.ts index e07b88c9f091..39b4946bdca8 100644 --- a/src/vs/workbench/contrib/chat/common/chatUrlFetchingConfirmation.ts +++ b/src/vs/workbench/contrib/chat/common/chatUrlFetchingConfirmation.ts @@ -87,10 +87,13 @@ export class ChatUrlFetchingConfirmationContribution implements ILanguageModelTo return []; } + //remove query strings + const urlsWithoutQuery = urls.map(u => u.split('?')[0]); + const actions: ILanguageModelToolConfirmationActions[] = []; // Get unique URLs (may have duplicates) - const uniqueUrls = Array.from(new Set(urls)).map(u => URI.parse(u)); + const uniqueUrls = Array.from(new Set(urlsWithoutQuery)).map(u => URI.parse(u)); // For each URL, get its patterns const urlPatterns = new ResourceMap(uniqueUrls.map(u => [u, extractUrlPatterns(u)] as const)); From db2fe74334fc0f5057ab9872b4b04274d93fa080 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 16 Dec 2025 14:09:35 -0800 Subject: [PATCH 25/37] debug: fix layout shift when hovering (#283917) Closes #240376 --- .../contrib/debug/browser/media/debugViewlet.css | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index fbf236948bf8..ed6b002dbc67 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -110,8 +110,14 @@ } .debug-pane .debug-call-stack .thread, -.debug-pane .debug-call-stack .session { +.debug-pane .debug-call-stack .session, +.debug-pane .debug-call-stack .stack-frame { display: flex; + padding-right: 12px; +} + +.debug-pane .debug-call-stack .thread, +.debug-pane .debug-call-stack .session { align-items: center; } @@ -143,7 +149,6 @@ .debug-pane .monaco-list-row .monaco-action-bar { display: none; flex-shrink: 0; - margin-right: 6px; } .debug-pane .monaco-list-row:hover .monaco-action-bar, @@ -163,8 +168,6 @@ .debug-pane .debug-call-stack .stack-frame { overflow: hidden; text-overflow: ellipsis; - padding-right: 0.8em; - display: flex; } .debug-pane .debug-call-stack .stack-frame.label { @@ -185,7 +188,6 @@ .debug-pane .debug-call-stack .stack-frame > .file { display: flex; overflow: hidden; - flex-wrap: wrap; justify-content: flex-end; } From 08dffa4a13bc32b847773aa1bd8efa0db416ef7d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:11:04 +0000 Subject: [PATCH 26/37] Git - remove the remaining instances of `config.multiDiffEditor.experimental.enabled` (#283919) --- extensions/git/package.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index e2b910e92089..973069efcf95 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2092,12 +2092,12 @@ }, { "command": "git.viewStagedChanges", - "when": "scmProvider == git && scmResourceGroup == index && config.multiDiffEditor.experimental.enabled", + "when": "scmProvider == git && scmResourceGroup == index", "group": "inline@1" }, { "command": "git.viewChanges", - "when": "scmProvider == git && scmResourceGroup == workingTree && config.multiDiffEditor.experimental.enabled", + "when": "scmProvider == git && scmResourceGroup == workingTree", "group": "inline@1" }, { @@ -2152,7 +2152,7 @@ }, { "command": "git.viewUntrackedChanges", - "when": "scmProvider == git && scmResourceGroup == untracked && config.multiDiffEditor.experimental.enabled", + "when": "scmProvider == git && scmResourceGroup == untracked", "group": "inline@1" }, { @@ -2711,7 +2711,7 @@ { "command": "git.timeline.viewCommit", "group": "inline", - "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection && config.multiDiffEditor.experimental.enabled" + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection" }, { "command": "git.timeline.openDiff", @@ -2721,7 +2721,7 @@ { "command": "git.timeline.viewCommit", "group": "1_actions@2", - "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection && config.multiDiffEditor.experimental.enabled" + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection" }, { "command": "git.timeline.compareWithSelected", @@ -2986,7 +2986,6 @@ }, { "command": "git.stashView", - "when": "config.multiDiffEditor.experimental.enabled", "group": "5_preview@1" } ], From 0b983d95c1ccf22e34e5438edeae2314d377ff87 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 16 Dec 2025 16:16:17 -0600 Subject: [PATCH 27/37] exclude mcp output from accessible view (#283889) fixes #281598 --- .../contrib/chat/browser/chatAccessibilityProvider.ts | 9 +++++++-- .../contrib/chat/browser/chatResponseAccessibleView.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts index 6a970d45271d..8ab09146f3a0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -16,7 +16,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { migrateLegacyTerminalToolSpecificData } from '../common/chat.js'; import { IChatToolInvocation } from '../common/chatService.js'; import { IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; -import { toolContentToA11yString } from '../common/languageModelToolsService.js'; +import { isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../common/languageModelToolsService.js'; import { CancelChatActionId } from './actions/chatExecuteActions.js'; import { AcceptToolConfirmationActionId } from './actions/chatToolActions.js'; import { ChatTreeItem } from './chat.js'; @@ -30,9 +30,14 @@ export const getToolConfirmationAlert = (accessor: ServicesAccessor, toolInvocat const text = toolInvocation.map(v => { const state = v.state.get(); if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + const detail = isToolResultInputOutputDetails(state.resultDetails) + ? state.resultDetails.input + : isToolResultOutputDetails(state.resultDetails) + ? undefined + : toolContentToA11yString(state.contentForModel); return { title: localize('toolPostApprovalTitle', "Approve results of tool"), - detail: toolContentToA11yString(state.contentForModel), + detail: detail, }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index aed067783447..572aa4143052 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -16,7 +16,7 @@ import { migrateLegacyTerminalToolSpecificData } from '../common/chat.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatToolInvocation } from '../common/chatService.js'; import { isResponseVM } from '../common/chatViewModel.js'; -import { toolContentToA11yString } from '../common/languageModelToolsService.js'; +import { isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../common/languageModelToolsService.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from './chat.js'; export class ChatResponseAccessibleView implements IAccessibleViewImplementation { @@ -111,7 +111,12 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi } responseContent += `\n${message}\n`; } else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { - responseContent += localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", toolInvocation.toolId) + toolContentToA11yString(state.contentForModel) + '\n'; + const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails) + ? state.resultDetails.input + : isToolResultOutputDetails(state.resultDetails) + ? undefined + : toolContentToA11yString(state.contentForModel); + responseContent += localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", toolInvocation.toolId) + (postApprovalDetails ?? '') + '\n'; } else { const resultDetails = IChatToolInvocation.resultDetails(toolInvocation); if (resultDetails && 'input' in resultDetails) { From 3f3ae0c458f75c4cff27905f5e3e66ee1ad32e57 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 16 Dec 2025 14:24:45 -0800 Subject: [PATCH 28/37] remote: fix connection listener leak (#283922) This function is only called though AbstractExtensionService._initialize once, so registering to the instance is fine. Closes #247610 --- .../services/extensions/browser/extensionService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 880beae09165..364ceaae0c03 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -193,12 +193,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten // monitor for breakage const connection = this._remoteAgentService.getConnection(); if (connection) { - connection.onDidStateChange(async (e) => { + this._register(connection.onDidStateChange(async (e) => { if (e.type === PersistentConnectionEventType.ConnectionLost) { this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority); } - }); - connection.onReconnecting(() => this._resolveAuthorityAgain()); + })); + this._register(connection.onReconnecting(() => this._resolveAuthorityAgain())); } return this._resolveExtensionsDefault(emitter); From a32f7d1ff0af815dc136b849baae966f35caeaad Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:29:55 +0000 Subject: [PATCH 29/37] Export terminal chat context key strings to prevent hardcoded strings and layering violations (#283907) --- .../contrib/terminal/browser/terminalMenus.ts | 3 ++- .../contrib/terminal/browser/terminalTabbedView.ts | 3 ++- .../contrib/terminal/common/terminalContextKey.ts | 3 ++- .../contrib/terminal/terminalContribExports.ts | 11 ++++++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 9301ea2d22fc..b0860ecc69ca 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -20,6 +20,7 @@ import { ACTIVE_GROUP, AUX_WINDOW_GROUP, SIDE_GROUP } from '../../../services/ed import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { HasSpeechProvider } from '../../speech/common/speechService.js'; import { hasKey } from '../../../../base/common/types.js'; +import { TerminalContribContextKeyStrings } from '../terminalContribExports.js'; export const enum TerminalContextMenuGroup { Chat = '0_chat', @@ -410,7 +411,7 @@ export function setupTerminalMenus(): void { group: 'navigation', order: 0, when: ContextKeyExpr.and( - ContextKeyExpr.not('hasHiddenChatTerminals'), + ContextKeyExpr.not(TerminalContribContextKeyStrings.ChatHasHiddenTerminals), ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), ContextKeyExpr.has(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.or( diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 56a03cc03fb9..b3d324928415 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -27,6 +27,7 @@ import { TerminalTabsChatEntry } from './terminalTabsChatEntry.js'; import { containsDragType } from '../../../../platform/dnd/browser/dnd.js'; import { getTerminalResourcesFromDragEvent, parseTerminalUri } from './terminalUri.js'; import type { IProcessDetails } from '../../../../platform/terminal/common/terminalProcess.js'; +import { TerminalContribContextKeyStrings } from '../terminalContribExports.js'; const $ = dom.$; @@ -143,7 +144,7 @@ export class TerminalTabbedView extends Disposable { })); this._register(contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(new Set(['hasHiddenChatTerminals']))) { + if (e.affectsSome(new Set([TerminalContribContextKeyStrings.ChatHasHiddenTerminals]))) { this._refreshShowTabs(); this._updateChatTerminalsEntry(); } diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 94cd0773ac09..82b0adcfc2d9 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -7,6 +7,7 @@ import { localize } from '../../../../nls.js'; import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; import { TERMINAL_VIEW_ID } from './terminal.js'; +import { TerminalContribContextKeyStrings } from '../terminalContribExports.js'; export const enum TerminalContextKeyStrings { IsOpen = 'terminalIsOpen', @@ -146,7 +147,7 @@ export namespace TerminalContextKeys { export const shouldShowViewInlineActions = ContextKeyExpr.and( ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), ContextKeyExpr.notEquals(`config.${TerminalSettingId.TabsHideCondition}`, 'never'), - ContextKeyExpr.not('hasHiddenChatTerminals'), + ContextKeyExpr.not(TerminalContribContextKeyStrings.ChatHasHiddenTerminals), ContextKeyExpr.or( ContextKeyExpr.not(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.and( diff --git a/src/vs/workbench/contrib/terminal/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/terminalContribExports.ts index a8fce413d4fa..68e715d04453 100644 --- a/src/vs/workbench/contrib/terminal/terminalContribExports.ts +++ b/src/vs/workbench/contrib/terminal/terminalContribExports.ts @@ -7,7 +7,7 @@ import type { IConfigurationNode } from '../../../platform/configuration/common/ import { TerminalAccessibilityCommandId, defaultTerminalAccessibilityCommandsToSkipShell } from '../terminalContrib/accessibility/common/terminal.accessibility.js'; import { terminalAccessibilityConfiguration } from '../terminalContrib/accessibility/common/terminalAccessibilityConfiguration.js'; import { terminalAutoRepliesConfiguration } from '../terminalContrib/autoReplies/common/terminalAutoRepliesConfiguration.js'; -import { TerminalChatCommandId } from '../terminalContrib/chat/browser/terminalChat.js'; +import { TerminalChatCommandId, TerminalChatContextKeyStrings } from '../terminalContrib/chat/browser/terminalChat.js'; import { terminalInitialHintConfiguration } from '../terminalContrib/chat/common/terminalInitialHintConfiguration.js'; import { terminalChatAgentToolsConfiguration, TerminalChatAgentToolsSettingId } from '../terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.js'; import { terminalCommandGuideConfiguration } from '../terminalContrib/commandGuide/common/terminalCommandGuideConfiguration.js'; @@ -46,6 +46,15 @@ export const enum TerminalContribSettingId { OutputLocation = TerminalChatAgentToolsSettingId.OutputLocation } +// HACK: Export some context key strings from `terminalContrib/` that are depended upon elsewhere. +// These are soft layer breakers between `terminal/` and `terminalContrib/` but there are +// difficulties in removing the dependency. These are explicitly defined here to avoid an eslint +// line override. +export const enum TerminalContribContextKeyStrings { + ChatHasTerminals = TerminalChatContextKeyStrings.ChatHasTerminals, + ChatHasHiddenTerminals = TerminalChatContextKeyStrings.ChatHasHiddenTerminals, +} + // Export configuration schemes from terminalContrib - this is an exception to the eslint rule since // they need to be declared at part of the rest of the terminal configuration export const terminalContribConfiguration: IConfigurationNode['properties'] = { From d64abc5368b4458a3b9e0934852ae257de564f8a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 16 Dec 2025 15:08:55 -0800 Subject: [PATCH 30/37] mcp: fix flakes in McpStdioStateHandler tests (#283925) * mcp: fix flakes in McpStdioStateHandler tests Closes #254921 Closes #253370 * fix compile --- .../contrib/mcp/test/node/mcpStdioStateHandler.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts b/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts index 9bee4963b01f..e759a24cec49 100644 --- a/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts +++ b/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts @@ -46,7 +46,7 @@ suite('McpStdioStateHandler', () => { process.on('SIGTERM', () => process.stdout.write('SIGTERM received')); `); - child.stdin.write('Hello MCP!'); + await new Promise(r => child.stdin.write('Hello MCP!', () => r())); handler.stop(); const result = await output; assert.strictEqual(result.trim(), 'Data received: Hello MCP!'); @@ -59,7 +59,9 @@ suite('McpStdioStateHandler', () => { process.stdin.on('end', () => process.stdout.write('stdin ended\\n')); process.stdin.resume(); process.on('SIGTERM', () => { - process.stdout.write('SIGTERM received', () => process.exit(0)); + process.stdout.write('SIGTERM received', () => { + process.stdout.end(() => process.exit(0)); + }); }); `); From 9dc98a2fd6e4409f2c490cb2522566dbb105cf9d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 16 Dec 2025 15:42:14 -0800 Subject: [PATCH 31/37] debug: fix webview debugging breaking with >256KB messages (#283936) --- src/vs/base/parts/ipc/node/ipc.net.ts | 12 ++++++++---- .../debug/electron-main/extensionHostDebugIpc.ts | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index e58f678f9860..9a508286e23b 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -22,10 +22,12 @@ export function upgradeToISocket(req: http.IncomingMessage, socket: Socket, { debugLabel, skipWebSocketFrames = false, disableWebSocketCompression = false, + enableMessageSplitting = true, }: { debugLabel: string; skipWebSocketFrames?: boolean; disableWebSocketCompression?: boolean; + enableMessageSplitting?: boolean; }): NodeSocket | WebSocketNodeSocket | undefined { if (req.headers.upgrade === undefined || req.headers.upgrade.toLowerCase() !== 'websocket') { socket.end('HTTP/1.1 400 Bad Request'); @@ -78,7 +80,7 @@ export function upgradeToISocket(req: http.IncomingMessage, socket: Socket, { if (skipWebSocketFrames) { return new NodeSocket(socket, debugLabel); } else { - return new WebSocketNodeSocket(new NodeSocket(socket, debugLabel), permessageDeflate, null, true); + return new WebSocketNodeSocket(new NodeSocket(socket, debugLabel), permessageDeflate, null, true, enableMessageSplitting); } } @@ -295,6 +297,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket, ISocketT private readonly _incomingData: ChunkStream; private readonly _onData = this._register(new Emitter()); private readonly _onClose = this._register(new Emitter()); + private readonly _maxSocketMessageLength: number; private _isEnded = false; private readonly _state = { @@ -331,9 +334,10 @@ export class WebSocketNodeSocket extends Disposable implements ISocket, ISocketT * @param inflateBytes "Seed" zlib inflate with these bytes. * @param recordInflateBytes Record all bytes sent to inflate */ - constructor(socket: NodeSocket, permessageDeflate: boolean, inflateBytes: VSBuffer | null, recordInflateBytes: boolean) { + constructor(socket: NodeSocket, permessageDeflate: boolean, inflateBytes: VSBuffer | null, recordInflateBytes: boolean, enableMessageSplitting = true) { super(); this.socket = socket; + this._maxSocketMessageLength = enableMessageSplitting ? Constants.MaxWebSocketMessageLength : Infinity; this.traceSocketEvent(SocketDiagnosticsEventType.Created, { type: 'WebSocketNodeSocket', permessageDeflate, inflateBytesLength: inflateBytes?.byteLength || 0, recordInflateBytes }); this._flowManager = this._register(new WebSocketFlowManager( this, @@ -404,8 +408,8 @@ export class WebSocketNodeSocket extends Disposable implements ISocket, ISocketT let start = 0; while (start < buffer.byteLength) { - this._flowManager.writeMessage(buffer.slice(start, Math.min(start + Constants.MaxWebSocketMessageLength, buffer.byteLength)), { compressed: true, opcode: 0x02 /* Binary frame */ }); - start += Constants.MaxWebSocketMessageLength; + this._flowManager.writeMessage(buffer.slice(start, Math.min(start + this._maxSocketMessageLength, buffer.byteLength)), { compressed: true, opcode: 0x02 /* Binary frame */ }); + start += this._maxSocketMessageLength; } } diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index b9db5bcf021b..91674b2f9ff2 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -84,6 +84,7 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens } const upgraded = upgradeToISocket(req, socket as Socket, { debugLabel: 'extension-host-cdp-' + generateUuid(), + enableMessageSplitting: false, }); if (upgraded) { From 8d8290957cad54f0633aea1ba1f373bb5e231628 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 16 Dec 2025 16:41:44 -0800 Subject: [PATCH 32/37] Restore tool documentation (#283939) Fix #256663 --- .../contrib/chat/browser/contrib/chatInputCompletions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index c13845dbf6a7..be2f44b44f5f 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -1207,6 +1207,7 @@ class ToolCompletions extends Disposable { } let detail: string | undefined; + let documentation: string | undefined; let name: string; if (item instanceof ToolSet) { @@ -1217,6 +1218,7 @@ class ToolCompletions extends Disposable { const source = item.source; detail = localize('tool_source_completion', "{0}: {1}", source.label, item.displayName); name = item.toolReferenceName ?? item.displayName; + documentation = item.userDescription ?? item.modelDescription; } if (usedNames.has(name)) { @@ -1228,6 +1230,7 @@ class ToolCompletions extends Disposable { label: withLeader, range, detail, + documentation, insertText: withLeader + ' ', kind: CompletionItemKind.Tool, sortText: 'z', From 1385867afffcad6265af14a2de7f852f79544f50 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 16 Dec 2025 16:42:43 -0800 Subject: [PATCH 33/37] Implement onDidBackgroundSession (#283935) For https://github.com/microsoft/vscode/pull/283901 --- src/vs/workbench/contrib/chat/browser/chat.ts | 12 +++++++++++- .../contrib/chat/browser/chatWidget.ts | 7 ++++--- .../contrib/chat/browser/chatWidgetService.ts | 17 +++++++++++++++++ .../contrib/chat/test/browser/mockChatWidget.ts | 1 + 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 46e0cbf135b5..5ec9152d2edf 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -45,6 +45,11 @@ export interface IChatWidgetService { readonly onDidAddWidget: Event; + /** + * Fires when a chat session is no longer open in any chat widget. + */ + readonly onDidBackgroundSession: Event; + /** * Reveals the widget, focusing its input unless `preserveFocus` is true. */ @@ -209,9 +214,14 @@ export interface IChatAcceptInputOptions { storeToHistory?: boolean; } +export interface IChatWidgetViewModelChangeEvent { + readonly previousSessionResource: URI | undefined; + readonly currentSessionResource: URI | undefined; +} + export interface IChatWidget { readonly domNode: HTMLElement; - readonly onDidChangeViewModel: Event; + readonly onDidChangeViewModel: Event; readonly onDidAcceptInput: Event; readonly onDidHide: Event; readonly onDidShow: Event; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 680012c435bf..62eac16754c4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -75,7 +75,7 @@ import { PromptsConfig } from '../common/promptSyntax/config/config.js'; import { IHandOff, PromptHeader, Target } from '../common/promptSyntax/promptFileParser.js'; import { IPromptsService } from '../common/promptSyntax/service/promptsService.js'; import { handleModeSwitch } from './actions/chatActions.js'; -import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from './chat.js'; +import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewModelChangeEvent, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from './chat.js'; import { ChatAccessibilityProvider } from './chatAccessibilityProvider.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { ChatSuggestNextWidget } from './chatContentParts/chatSuggestNextWidget.js'; @@ -184,7 +184,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidFocus = this._register(new Emitter()); readonly onDidFocus = this._onDidFocus.event; - private _onDidChangeViewModel = this._register(new Emitter()); + private _onDidChangeViewModel = this._register(new Emitter()); readonly onDidChangeViewModel = this._onDidChangeViewModel.event; private _onDidScroll = this._register(new Emitter()); @@ -292,6 +292,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return; } + const previousSessionResource = this._viewModel?.sessionResource; this.viewModelDisposables.clear(); this._viewModel = viewModel; @@ -302,7 +303,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.logService.debug('ChatWidget#setViewModel: no viewModel'); } - this._onDidChangeViewModel.fire(); + this._onDidChangeViewModel.fire({ previousSessionResource, currentSessionResource: this._viewModel?.sessionResource }); } get viewModel() { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/chatWidgetService.ts index 2430382256e8..23b50acbd500 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidgetService.ts @@ -13,6 +13,7 @@ import { ILayoutService } from '../../../../platform/layout/browser/layoutServic import { ACTIVE_GROUP, IEditorService, type PreferredGroup } from '../../../../workbench/services/editor/common/editorService.js'; import { IEditorGroup, IEditorGroupsService, isEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { IChatService } from '../common/chatService.js'; import { ChatAgentLocation } from '../common/constants.js'; import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuickChatService, isIChatViewViewContext } from './chat.js'; import { ChatEditor, IChatEditorOptions } from './chatEditor.js'; @@ -29,12 +30,16 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService private readonly _onDidAddWidget = this._register(new Emitter()); readonly onDidAddWidget = this._onDidAddWidget.event; + private readonly _onDidBackgroundSession = this._register(new Emitter()); + readonly onDidBackgroundSession = this._onDidBackgroundSession.event; + constructor( @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IViewsService private readonly viewsService: IViewsService, @IQuickChatService private readonly quickChatService: IQuickChatService, @ILayoutService private readonly layoutService: ILayoutService, @IEditorService private readonly editorService: IEditorService, + @IChatService private readonly chatService: IChatService, ) { super(); } @@ -228,6 +233,18 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService return combinedDisposable( newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)), + newWidget.onDidChangeViewModel(({ previousSessionResource, currentSessionResource }) => { + if (!previousSessionResource || (currentSessionResource && isEqual(previousSessionResource, currentSessionResource))) { + return; + } + + // Timeout to ensure it wasn't just moving somewhere else + void timeout(200).then(() => { + if (!this.getWidgetBySessionResource(previousSessionResource) && this.chatService.getSession(previousSessionResource)) { + this._onDidBackgroundSession.fire(previousSessionResource); + } + }); + }), toDisposable(() => this._widgets.splice(this._widgets.indexOf(newWidget), 1)) ); } diff --git a/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts b/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts index 4832253d5ba5..9293a8172213 100644 --- a/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts +++ b/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts @@ -11,6 +11,7 @@ import { ChatAgentLocation } from '../../common/constants.js'; export class MockChatWidgetService implements IChatWidgetService { readonly onDidAddWidget: Event = Event.None; + readonly onDidBackgroundSession: Event = Event.None; readonly _serviceBrand: undefined; From ee735412792a94a8fdee8bb4e78a0edd72503303 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Tue, 16 Dec 2025 17:14:30 -0800 Subject: [PATCH 34/37] Add some padding to align checkboxes when tree items have no children (#283916) --- src/vs/platform/quickinput/browser/media/quickInput.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 7d6e69a3d700..2574381f5548 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -395,6 +395,10 @@ display: none !important; } +.quick-input-tree.quick-input-tree-flat .monaco-checkbox { + margin-left: 6px; +} + .quick-input-tree .quick-input-tree-entry { box-sizing: border-box; overflow: hidden; From 8b949a820c300cd04584b764eb1cef86cf2b2410 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 16 Dec 2025 17:48:44 -0800 Subject: [PATCH 35/37] testing: some polish on test coverage navigation (#283938) Closes #258967 --- .../browser/codeCoverageDecorations.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index 63fa40bc3ccd..a98a27cf60d2 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -636,11 +636,18 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { this.actionBar = this._register(instaService.createInstance(ActionBar, this._domNode.toolbar, { orientation: ActionsOrientation.HORIZONTAL, actionViewItemProvider: (action, options) => { - const vm = new CodiconActionViewItem(undefined, action, options); if (action instanceof ActionWithIcon) { + if (action.iconOnly) { + action.class = ThemeIcon.asClassName(action.icon); + return new ActionViewItem(undefined, action, { ...options, label: false, icon: true }); + } + + const vm = new CodiconActionViewItem(undefined, action, options); vm.themeIcon = action.icon; + return vm; } - return vm; + + return undefined; } })); @@ -704,8 +711,8 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { const toggleAction = new ActionWithIcon( 'toggleInline', this.coverage.showInline.get() - ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') - : localize('testing.showInlineCoverage', 'Show Inline Coverage'), + ? localize('testing.hideInlineCoverage', 'Hide Inline') + : localize('testing.showInlineCoverage', 'Show Inline'), testingCoverageReport, undefined, () => this.coverage.showInline.set(!this.coverage.showInline.get(), undefined), @@ -716,25 +723,28 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { toggleAction.tooltip = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; } - this.actionBar.push(toggleAction); - + const hasUncoveredStmt = current.coverage.statement.covered < current.coverage.statement.total; // Navigation buttons for missed coverage lines this.actionBar.push(new ActionWithIcon( 'goToPreviousMissed', GO_TO_PREVIOUS_MISSED_LINE_TITLE.value, Codicon.arrowUp, - undefined, + hasUncoveredStmt, () => this.commandService.executeCommand(TestCommandId.CoverageGoToPreviousMissedLine), + true, )); this.actionBar.push(new ActionWithIcon( 'goToNextMissed', GO_TO_NEXT_MISSED_LINE_TITLE.value, Codicon.arrowDown, - undefined, + hasUncoveredStmt, () => this.commandService.executeCommand(TestCommandId.CoverageGoToNextMissedLine), + true, )); + this.actionBar.push(toggleAction); + if (current.testId) { const testItem = current.coverage.fromResult.getTestById(current.testId.toString()); assert(!!testItem, 'got coverage for an unreported test'); @@ -848,7 +858,7 @@ registerAction2(class ToggleCoverageToolbar extends Action2 { constructor() { super({ id: TestCommandId.CoverageToggleToolbar, - title: localize2('testing.toggleToolbarTitle', "Test Coverage Toolbar"), + title: localize2('testing.toggleToolbarTitle', "Show Test Coverage Toolbar"), metadata: { description: localize2('testing.toggleToolbarDesc', 'Toggle the sticky coverage bar in the editor.') }, @@ -859,7 +869,7 @@ registerAction2(class ToggleCoverageToolbar extends Action2 { menu: [ { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, { id: MenuId.StickyScrollContext, when: TestingContextKeys.isTestCoverageOpen }, - { id: MenuId.EditorTitle, when: TestingContextKeys.hasCoverageInFile, group: 'coverage@1' }, + { id: MenuId.EditorTitle, when: TestingContextKeys.hasCoverageInFile, group: 'coverage', order: 1 }, ] }); } @@ -1020,7 +1030,7 @@ registerAction2(class GoToNextMissedCoverageLine extends Action2 { }, menu: [ { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, - { id: MenuId.EditorTitle, when: TestingContextKeys.hasCoverageInFile, group: 'coverage@2' }, + { id: MenuId.EditorTitle, when: TestingContextKeys.hasCoverageInFile, group: 'coverage', order: 2 }, ] }); } @@ -1056,7 +1066,7 @@ registerAction2(class GoToPreviousMissedCoverageLine extends Action2 { }, menu: [ { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, - { id: MenuId.EditorTitle, when: TestingContextKeys.hasCoverageInFile, group: 'coverage@3' }, + { id: MenuId.EditorTitle, when: TestingContextKeys.hasCoverageInFile, group: 'coverage', order: 3 }, ] }); } @@ -1074,7 +1084,7 @@ registerAction2(class GoToPreviousMissedCoverageLine extends Action2 { }); class ActionWithIcon extends Action { - constructor(id: string, title: string, public readonly icon: ThemeIcon, enabled: boolean | undefined, run: () => void) { + constructor(id: string, title: string, public readonly icon: ThemeIcon, enabled: boolean | undefined, run: () => void, public iconOnly = false) { super(id, title, undefined, enabled, run); } } From 2976d9514e29d7dea2da2bd0e6653d6a00ae8d2b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 17 Dec 2025 07:52:10 +0000 Subject: [PATCH 36/37] Chat - fix hovering on the working set item when there are no actions (#283989) --- src/vs/workbench/contrib/chat/browser/media/chat.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index b145d368fb48..238a580a2fd6 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -797,9 +797,9 @@ have to be updated for changes to the rules above, or to support more deeply nes display: none; } -.interactive-session .chat-editing-session .monaco-list-row:hover .chat-collapsible-list-action-bar, -.interactive-session .chat-editing-session .monaco-list-row.focused .chat-collapsible-list-action-bar, -.interactive-session .chat-editing-session .monaco-list-row.selected .chat-collapsible-list-action-bar { +.interactive-session .chat-editing-session .monaco-list-row:hover .chat-collapsible-list-action-bar:not(.has-no-actions), +.interactive-session .chat-editing-session .monaco-list-row.focused .chat-collapsible-list-action-bar:not(.has-no-actions), +.interactive-session .chat-editing-session .monaco-list-row.selected .chat-collapsible-list-action-bar:not(.has-no-actions) { display: inherit; } From 1aba7da3bf7b7ee2a36acf3a9fb78ec4cc8a7f33 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 17 Dec 2025 09:38:49 +0000 Subject: [PATCH 37/37] Engineering - add missing variable to the test pipeline (#283991) --- build/azure-pipelines/product-build-macos.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/azure-pipelines/product-build-macos.yml b/build/azure-pipelines/product-build-macos.yml index 3f61b794ac04..57d88a6c3d75 100644 --- a/build/azure-pipelines/product-build-macos.yml +++ b/build/azure-pipelines/product-build-macos.yml @@ -28,6 +28,8 @@ variables: value: ${{ parameters.VSCODE_QUALITY }} - name: VSCODE_CIBUILD value: ${{ in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }} + - name: VSCODE_STEP_ON_IT + value: false - name: skipComponentGovernanceDetection value: true - name: ComponentDetection.Timeout