From 21049d3dc8580a5c9f2fd79efa118a3d2e71ef79 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 11:55:52 -0800 Subject: [PATCH 01/50] add openPullRequest command and update related logic in GitHub extension --- extensions/github/package.json | 17 +++++++++++- extensions/github/package.nls.json | 1 + extensions/github/src/commands.ts | 26 +++++++++++++++++++ .../changesView/browser/changesView.ts | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index a9ba2e87d3087..0e33a21f9186a 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -74,6 +74,11 @@ "command": "github.createPullRequest", "title": "%command.createPullRequest%", "icon": "$(git-pull-request)" + }, + { + "command": "github.openPullRequest", + "title": "%command.openPullRequest%", + "icon": "$(git-pull-request)" } ], "continueEditSession": [ @@ -95,6 +100,10 @@ "command": "github.createPullRequest", "when": "false" }, + { + "command": "github.openPullRequest", + "when": "false" + }, { "command": "github.graph.openOnGitHub", "when": "false" @@ -179,7 +188,13 @@ "command": "github.createPullRequest", "group": "navigation", "order": 1, - "when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli" + "when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli && !github.hasOpenPullRequest" + }, + { + "command": "github.openPullRequest", + "group": "navigation", + "order": 1, + "when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli && github.hasOpenPullRequest" } ] }, diff --git a/extensions/github/package.nls.json b/extensions/github/package.nls.json index ced536e4bd7c6..4acc8acabcbab 100644 --- a/extensions/github/package.nls.json +++ b/extensions/github/package.nls.json @@ -6,6 +6,7 @@ "command.openOnGitHub": "Open on GitHub", "command.openOnVscodeDev": "Open in vscode.dev", "command.createPullRequest": "Create Pull Request", + "command.openPullRequest": "Open Pull Request", "config.branchProtection": "Controls whether to query repository rules for GitHub repositories", "config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.", "config.gitProtocol": "Controls which protocol is used to clone a GitHub repository", diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 496772ededf82..eee730b853ade 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -8,6 +8,7 @@ import { API as GitAPI, RefType, Repository } from './typings/git.js'; import { publishRepository } from './publish.js'; import { DisposableStore, getRepositoryFromUrl } from './util.js'; import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links.js'; +import { getOctokit } from './auth.js'; async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) { try { @@ -89,6 +90,27 @@ async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | u } } + // Check if a PR already exists for this branch + try { + const octokit = await getOctokit(); + const { data: pullRequests } = await octokit.pulls.list({ + owner: remoteInfo.owner, + repo: remoteInfo.repo, + head: `${remoteInfo.owner}:${head.name}`, + state: 'open', + }); + + if (pullRequests.length > 0) { + vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', true); + vscode.env.openExternal(vscode.Uri.parse(pullRequests[0].html_url)); + return; + } + } catch { + // If the API call fails, fall through to open the creation URL + } + + vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false); + // Build the GitHub PR creation URL // Format: https://github.com/owner/repo/compare/base...head const prUrl = `https://github.com/${remoteInfo.owner}/${remoteInfo.repo}/compare/${head.name}?expand=1`; @@ -181,5 +203,9 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return createPullRequest(gitAPI, sessionResource, sessionMetadata); })); + disposables.add(vscode.commands.registerCommand('github.openPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => { + return createPullRequest(gitAPI, sessionResource, sessionMetadata); + })); + return disposables; } diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index d437e6e19a032..e09785f56827d 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -562,7 +562,7 @@ export class ChangesViewPane extends ViewPane { ); return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } - if (action.id === 'github.createPullRequest') { + if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' }; } if (action.id === 'chatEditing.synchronizeChanges') { From 0d0acbcde2fbe3caf246625537853680a23fb12d Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 11:58:29 -0800 Subject: [PATCH 02/50] remove unnecessary context setting for open pull request in createPullRequest function --- extensions/github/src/commands.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index eee730b853ade..1ab189feae75a 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -109,8 +109,6 @@ async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | u // If the API call fails, fall through to open the creation URL } - vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false); - // Build the GitHub PR creation URL // Format: https://github.com/owner/repo/compare/base...head const prUrl = `https://github.com/${remoteInfo.owner}/${remoteInfo.repo}/compare/${head.name}?expand=1`; From 9bbe45657702fdd4e798709f68016f06c01d2d6e Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 14:24:50 -0800 Subject: [PATCH 03/50] add logging for session menu and button configuration in ChangesViewPane --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index e09785f56827d..fd6f40a2d6a02 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -545,16 +545,19 @@ export class ChangesViewPane extends ViewPane { this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); + const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; + console.log('[changesView] isSessionMenu:', isSessionMenu, 'menuId:', menuId.id, 'sessionResource:', sessionResource?.toString()); reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, this.actionsContainer!, - isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar, + menuId, { telemetrySource: 'changesView', menuOptions: isSessionMenu && sessionResource ? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] } : { shouldForwardArgs: true }, buttonConfigProvider: (action) => { + console.log('[changesView] buttonConfigProvider action:', action.id); if (action.id === 'chatEditing.viewChanges' || action.id === 'chatEditing.viewPreviousEdits' || action.id === 'chatEditing.viewAllSessionChanges' || action.id === 'chat.openSessionWorktreeInVSCode') { const diffStatsLabel = new MarkdownString( `+${added} -${removed}`, From 4dfa19f48bd660b34262bc56cb2902ced405676c Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 26 Feb 2026 23:28:20 +0100 Subject: [PATCH 04/50] Applying custom line heights after the edits are processed (#297999) * adding lineHeightsAdded array * polish * fixing the test * renaming to lineHeightAdded * adding code to update the line heights on deletion * polishing * polishing * polishing * using same line for from and to * adding unit test * fixing merge * fixing tests * modifying the comment * applying changes after * removing comments * fixing the tests * renaming the variable --- src/vs/editor/common/model/textModel.ts | 4 ++- src/vs/editor/common/textModelEvents.ts | 7 +++- .../editor/common/viewLayout/lineHeights.ts | 30 ++++------------- .../editor/common/viewLayout/linesLayout.ts | 5 ++- src/vs/editor/common/viewLayout/viewLayout.ts | 4 +-- .../editor/common/viewModel/viewModelImpl.ts | 26 +++++++++++++-- src/vs/editor/test/common/model/model.test.ts | 4 +-- .../common/viewLayout/lineHeights.test.ts | 32 ++++++++++++------- .../common/viewLayout/linesLayout.test.ts | 4 +-- 9 files changed, 67 insertions(+), 49 deletions(-) diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 4d3ae947ad6e5..8a276a599e410 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1572,7 +1572,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (editingLinesCnt < deletingLinesCnt) { // Must delete some lines const spliceStartLineNumber = startLineNumber + editingLinesCnt; - rawContentChanges.push(new ModelRawLinesDeleted(spliceStartLineNumber + 1, endLineNumber)); + const cnt = insertingLinesCnt - deletingLinesCnt; + const lastUntouchedLinePostEdit = newLineCount - lineCount - cnt + spliceStartLineNumber; + rawContentChanges.push(new ModelRawLinesDeleted(spliceStartLineNumber + 1, endLineNumber, lastUntouchedLinePostEdit)); } if (editingLinesCnt < insertingLinesCnt) { diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 4fa24afd2c1b0..ebbba09318334 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -397,10 +397,15 @@ export class ModelRawLinesDeleted { * At what line the deletion stopped (inclusive). */ public readonly toLineNumber: number; + /** + * The last unmodified line in the updated buffer after the deletion is made. + */ + public readonly lastUntouchedLinePostEdit: number; - constructor(fromLineNumber: number, toLineNumber: number) { + constructor(fromLineNumber: number, toLineNumber: number, lastUntouchedLinePostEdit: number) { this.fromLineNumber = fromLineNumber; this.toLineNumber = toLineNumber; + this.lastUntouchedLinePostEdit = lastUntouchedLinePostEdit; } } diff --git a/src/vs/editor/common/viewLayout/lineHeights.ts b/src/vs/editor/common/viewLayout/lineHeights.ts index 215d210f9fda1..94e81bf561d6c 100644 --- a/src/vs/editor/common/viewLayout/lineHeights.ts +++ b/src/vs/editor/common/viewLayout/lineHeights.ts @@ -21,7 +21,7 @@ type PendingChange = | { readonly kind: PendingChangeKind.InsertOrChange; readonly decorationId: string; readonly startLineNumber: number; readonly endLineNumber: number; readonly lineHeight: number } | { readonly kind: PendingChangeKind.Remove; readonly decorationId: string } | { readonly kind: PendingChangeKind.LinesDeleted; readonly fromLineNumber: number; readonly toLineNumber: number } - | { readonly kind: PendingChangeKind.LinesInserted; readonly fromLineNumber: number; readonly toLineNumber: number; readonly lineHeightsAdded: CustomLineHeightData[] }; + | { readonly kind: PendingChangeKind.LinesInserted; readonly fromLineNumber: number; readonly toLineNumber: number }; export class CustomLine { @@ -132,8 +132,8 @@ export class LineHeightsManager { this._hasPending = true; } - public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { - this._pendingChanges.push({ kind: PendingChangeKind.LinesInserted, fromLineNumber, toLineNumber, lineHeightsAdded }); + public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + this._pendingChanges.push({ kind: PendingChangeKind.LinesInserted, fromLineNumber, toLineNumber }); this._hasPending = true; } @@ -160,7 +160,7 @@ export class LineHeightsManager { break; case PendingChangeKind.LinesInserted: this._flushStagedDecorationChanges(stagedInserts); - this._doLinesInserted(change.fromLineNumber, change.toLineNumber, change.lineHeightsAdded, stagedInserts); + this._doLinesInserted(change.fromLineNumber, change.toLineNumber, stagedInserts); break; } } @@ -358,7 +358,7 @@ export class LineHeightsManager { } } - private _doLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[], stagedInserts: CustomLine[]): void { + private _doLinesInserted(fromLineNumber: number, toLineNumber: number, stagedInserts: CustomLine[]): void { const insertCount = toLineNumber - fromLineNumber + 1; const candidateStartIndexOfInsertion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber); let startIndexOfInsertion: number; @@ -374,22 +374,6 @@ export class LineHeightsManager { } else { startIndexOfInsertion = -(candidateStartIndexOfInsertion + 1); } - const maxLineHeightPerLine = new Map(); - for (const lineHeightAdded of lineHeightsAdded) { - for (let lineNumber = lineHeightAdded.startLineNumber; lineNumber <= lineHeightAdded.endLineNumber; lineNumber++) { - if (lineNumber >= fromLineNumber && lineNumber <= toLineNumber) { - const currentMax = maxLineHeightPerLine.get(lineNumber) ?? this._defaultLineHeight; - maxLineHeightPerLine.set(lineNumber, Math.max(currentMax, lineHeightAdded.lineHeight)); - } - } - this._doInsertOrChangeCustomLineHeight( - lineHeightAdded.decorationId, - lineHeightAdded.startLineNumber, - lineHeightAdded.endLineNumber, - lineHeightAdded.lineHeight, - stagedInserts - ); - } const toReAdd: CustomLineHeightData[] = []; const decorationsImmediatelyAfter = new Set(); for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { @@ -404,9 +388,7 @@ export class LineHeightsManager { } } const decorationsWithGaps = intersection(decorationsImmediatelyBefore, decorationsImmediatelyAfter); - const specialHeightToAdd = Array.from(maxLineHeightPerLine.values()).reduce((acc, height) => acc + height, 0); - const defaultHeightToAdd = (insertCount - maxLineHeightPerLine.size) * this._defaultLineHeight; - const prefixSumToAdd = specialHeightToAdd + defaultHeightToAdd; + const prefixSumToAdd = insertCount * this._defaultLineHeight; for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { this._orderedCustomLines[i].lineNumber += insertCount; this._orderedCustomLines[i].prefixSum += prefixSumToAdd; diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index cd69d95877d8f..033b7423db4c2 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -349,9 +349,8 @@ export class LinesLayout { * * @param fromLineNumber The line number at which the insertion started, inclusive * @param toLineNumber The line number at which the insertion ended, inclusive. - * @param lineHeightsAdded The custom line height data for the inserted lines. */ - public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { + public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { fromLineNumber = fromLineNumber | 0; toLineNumber = toLineNumber | 0; @@ -363,7 +362,7 @@ export class LinesLayout { this._arr[i].afterLineNumber += (toLineNumber - fromLineNumber + 1); } } - this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber, lineHeightsAdded); + this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber); } /** diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 048dc241eae7d..202187a4aadb2 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -243,8 +243,8 @@ export class ViewLayout extends Disposable implements IViewLayout { public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { this._linesLayout.onLinesDeleted(fromLineNumber, toLineNumber); } - public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { - this._linesLayout.onLinesInserted(fromLineNumber, toLineNumber, lineHeightsAdded); + public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + this._linesLayout.onLinesInserted(fromLineNumber, toLineNumber); } // ---- end view event handlers diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 299fb151446f4..f632d6950338f 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -355,6 +355,11 @@ export class ViewModel extends Disposable implements IViewModel { const lineBreaks = lineBreaksComputer.finalize(); const lineBreakQueue = new ArrayQueue(lineBreaks); + // Collect model line ranges that need custom line height computation. + // We defer this until after the loop because the coordinatesConverter + // relies on projections that may not yet reflect all changes in the batch. + const customLineHeightRangesToInsert: { fromLineNumber: number; toLineNumber: number }[] = []; + for (const change of changes) { switch (change.changeType) { case textModelEvents.RawContentChangedType.Flush: { @@ -370,6 +375,7 @@ export class ViewModel extends Disposable implements IViewModel { if (linesDeletedEvent !== null) { eventsCollector.emitViewEvent(linesDeletedEvent); this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); + customLineHeightRangesToInsert.push({ fromLineNumber: change.lastUntouchedLinePostEdit, toLineNumber: change.lastUntouchedLinePostEdit }); } hadOtherModelChange = true; break; @@ -379,7 +385,8 @@ export class ViewModel extends Disposable implements IViewModel { const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); if (linesInsertedEvent !== null) { eventsCollector.emitViewEvent(linesInsertedEvent); - this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber, this._getCustomLineHeightsForLines(change.fromLineNumberPostEdit, change.toLineNumberPostEdit)); + this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); + customLineHeightRangesToInsert.push({ fromLineNumber: change.fromLineNumberPostEdit, toLineNumber: change.toLineNumberPostEdit }); } hadOtherModelChange = true; break; @@ -394,11 +401,13 @@ export class ViewModel extends Disposable implements IViewModel { } if (linesInsertedEvent) { eventsCollector.emitViewEvent(linesInsertedEvent); - this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber, this._getCustomLineHeightsForLines(change.lineNumberPostEdit, change.lineNumberPostEdit)); + this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); + customLineHeightRangesToInsert.push({ fromLineNumber: change.lineNumberPostEdit, toLineNumber: change.lineNumberPostEdit }); } if (linesDeletedEvent) { eventsCollector.emitViewEvent(linesDeletedEvent); this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); + customLineHeightRangesToInsert.push({ fromLineNumber: change.lineNumberPostEdit, toLineNumber: change.lineNumberPostEdit }); } break; } @@ -412,6 +421,19 @@ export class ViewModel extends Disposable implements IViewModel { if (versionId !== null) { this._lines.acceptVersionId(versionId); } + + // Apply deferred custom line heights now that projections are stable + if (customLineHeightRangesToInsert.length > 0) { + this.viewLayout.changeSpecialLineHeights((accessor: ILineHeightChangeAccessor) => { + for (const range of customLineHeightRangesToInsert) { + const customLineHeights = this._getCustomLineHeightsForLines(range.fromLineNumber, range.toLineNumber); + for (const data of customLineHeights) { + accessor.insertOrChangeCustomLineHeight(data.decorationId, data.startLineNumber, data.endLineNumber, data.lineHeight); + } + } + }); + } + this.viewLayout.onHeightMaybeChanged(); if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) { diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 72140c2636000..c518ee861522f 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -245,7 +245,7 @@ suite('Editor Model - Model', () => { assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 1, 'My Second Line', null), - new ModelRawLinesDeleted(2, 2), + new ModelRawLinesDeleted(2, 2, 1), ], 2, false, @@ -260,7 +260,7 @@ suite('Editor Model - Model', () => { assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 1, 'My Third Line', null), - new ModelRawLinesDeleted(2, 3), + new ModelRawLinesDeleted(2, 3, 1), ], 2, false, diff --git a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts index 48b50f306162a..695650dd8a355 100644 --- a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts @@ -182,7 +182,7 @@ suite('Editor ViewLayout - LineHeightsManager', () => { const manager = new LineHeightsManager(10, []); manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); - manager.onLinesInserted(3, 4, []); // Insert 2 lines at line 3 + manager.onLinesInserted(3, 4); // Insert 2 lines at line 3 assert.strictEqual(manager.heightForLineNumber(5), 10); assert.strictEqual(manager.heightForLineNumber(6), 10); @@ -195,7 +195,7 @@ suite('Editor ViewLayout - LineHeightsManager', () => { const manager = new LineHeightsManager(10, []); manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); - manager.onLinesInserted(6, 7, []); // Insert 2 lines at line 6 + manager.onLinesInserted(6, 7); // Insert 2 lines at line 6 assert.strictEqual(manager.heightForLineNumber(5), 20); assert.strictEqual(manager.heightForLineNumber(6), 20); @@ -267,9 +267,8 @@ suite('Editor ViewLayout - LineHeightsManager', () => { assert.strictEqual(manager.heightForLineNumber(2), 10); // Insert line 2 to line 2, with the same decoration ID 'decA' covering line 2 - manager.onLinesInserted(2, 2, [ - new CustomLineHeightData('decA', 2, 2, 30) - ]); + manager.onLinesInserted(2, 2); + manager.insertOrChangeCustomLineHeight('decA', 2, 2, 30); // After insertion, the decoration 'decA' now covers line 2 // Since insertOrChangeCustomLineHeight removes the old decoration first, @@ -349,7 +348,7 @@ suite('Editor ViewLayout - LineHeightsManager (auto-commit on read)', () => { // Caller A removes its decoration before any flush occurs. manager.removeCustomLineHeight('decA'); // Caller B triggers a structural change that causes queue flush in the middle of commit. - manager.onLinesInserted(1, 1, []); + manager.onLinesInserted(1, 1); // decA must stay removed. If queued inserts are not canceled on remove, decA incorrectly survives. assert.strictEqual(manager.heightForLineNumber(4), 10); @@ -381,7 +380,7 @@ suite('Editor ViewLayout - LineHeightsManager (auto-commit on read)', () => { manager.insertOrChangeCustomLineHeight('dec1', 2, 2, 20); manager.insertOrChangeCustomLineHeight('dec2', 5, 5, 30); // Step 3: insert 2 lines at line 3 (shifts dec2 from line 5 → 7) - manager.onLinesInserted(3, 4, []); + manager.onLinesInserted(3, 4); // Step 4: delete line 1 (shifts dec1 from line 2 → 1, dec2 from line 7 → 6) manager.onLinesDeleted(1, 1); // Step 5-6: remove the two decorations @@ -402,7 +401,7 @@ suite('Editor ViewLayout - LineHeightsManager (auto-commit on read)', () => { const manager = new LineHeightsManager(10, []); manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); // Insert 1 line at line 1 → dec1 shifts from 3 → 4 - manager.onLinesInserted(1, 1, []); + manager.onLinesInserted(1, 1); manager.removeCustomLineHeight('dec1'); // Read — no explicit commit assert.strictEqual(manager.heightForLineNumber(3), 10); @@ -442,7 +441,7 @@ suite('Editor ViewLayout - LineHeightsManager (auto-commit on read)', () => { const manager = new LineHeightsManager(10, []); manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); // Insert 2 lines at line 1 → dec1 moves from 3 → 5 - manager.onLinesInserted(1, 2, []); + manager.onLinesInserted(1, 2); // Delete line 1 → dec1 moves from 5 → 4 manager.onLinesDeleted(1, 1); // Read @@ -455,9 +454,9 @@ suite('Editor ViewLayout - LineHeightsManager (auto-commit on read)', () => { const manager = new LineHeightsManager(10, []); manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); // Insert 1 line at line 1 → dec1 at 3 → 4 - manager.onLinesInserted(1, 1, []); + manager.onLinesInserted(1, 1); // Insert 1 line at line 1 → dec1 at 4 → 5 - manager.onLinesInserted(1, 1, []); + manager.onLinesInserted(1, 1); // Read assert.strictEqual(manager.heightForLineNumber(5), 20); assert.strictEqual(manager.heightForLineNumber(3), 10); @@ -492,7 +491,7 @@ suite('Editor ViewLayout - LineHeightsManager (auto-commit on read)', () => { // Insert a decoration at line 3 (pending, not committed) manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); // Insert 2 lines before it at line 1 → should shift dec1 from 3 → 5 - manager.onLinesInserted(1, 2, []); + manager.onLinesInserted(1, 2); // Read assert.strictEqual(manager.heightForLineNumber(3), 10); assert.strictEqual(manager.heightForLineNumber(5), 20); @@ -524,4 +523,13 @@ suite('Editor ViewLayout - LineHeightsManager (auto-commit on read)', () => { assert.strictEqual(manager.heightForLineNumber(6), 30); assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(6), 110); }); + + test('deleting line 2 with lineHeightsRemoved re-adding at line 1 moves special line to line 1', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 2, 2, 20); + assert.strictEqual(manager.heightForLineNumber(2), 20); + manager.onLinesDeleted(2, 2); + manager.insertOrChangeCustomLineHeight('dec1', 1, 1, 20); + assert.strictEqual(manager.heightForLineNumber(1), 20); + }); }); diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index ceb624ac2740e..7bf20a78d8457 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -208,7 +208,7 @@ suite('Editor ViewLayout - LinesLayout', () => { // Insert two lines at the beginning // 10 lines // whitespace: - a(6,10) - linesLayout.onLinesInserted(1, 2, []); + linesLayout.onLinesInserted(1, 2); assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); @@ -909,7 +909,7 @@ suite('Editor ViewLayout - LinesLayout', () => { assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Insert a line before line 1 - linesLayout.onLinesInserted(1, 1, []); + linesLayout.onLinesInserted(1, 1); // whitespaces: d(3, 30), c(4, 20) assert.strictEqual(linesLayout.getWhitespacesCount(), 2); assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); From f1027ecc9a1d76cf5e9c009602d690210629a432 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 26 Feb 2026 23:33:43 +0100 Subject: [PATCH 05/50] support history (#298096) * support history * feedback --- .../chat/browser/newChatContextAttachments.ts | 7 + .../contrib/chat/browser/newChatViewPane.ts | 184 ++++++++++++++++-- 2 files changed, 177 insertions(+), 14 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts b/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts index 7219eaeafd80f..09b900d5a7285 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts @@ -54,6 +54,13 @@ export class NewChatContextAttachments extends Disposable { return this._attachedContext; } + setAttachments(entries: readonly IChatRequestVariableEntry[]): void { + this._attachedContext.length = 0; + this._attachedContext.push(...entries); + this._updateRendering(); + this._onDidChangeContext.fire(); + } + constructor( @IQuickInputService private readonly quickInputService: IQuickInputService, @ITextModelService private readonly textModelService: ITextModelService, diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 489cceb9cce5a..0395a03ae2f0b 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -23,6 +23,7 @@ import { SuggestController } from '../../../../editor/contrib/suggest/browser/su import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService, IContextKey, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -35,6 +36,7 @@ import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hover import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { localize } from '../../../../nls.js'; +import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ChatSessionPosition, getResourceForNewChatSession } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.js'; @@ -62,8 +64,14 @@ import { RepoPicker } from './repoPicker.js'; import { CloudModelPicker } from './modelPicker.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; import { SlashCommandHandler } from './slashCommands.js'; - -const STORAGE_KEY_LAST_MODEL = 'sessions.selectedModel'; +import { IChatModelInputState } from '../../../../workbench/contrib/chat/common/model/chatModel.js'; +import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js'; +import { ChatHistoryNavigator } from '../../../../workbench/contrib/chat/common/widget/chatWidgetHistoryService.js'; +import { IHistoryNavigationWidget } from '../../../../base/browser/history.js'; +import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js'; + +const STORAGE_KEY_DRAFT_STATE = 'sessions.draftState'; const MIN_EDITOR_HEIGHT = 50; const MAX_EDITOR_HEIGHT = 200; @@ -85,7 +93,7 @@ interface INewChatWidgetOptions { * This widget is shown only in the empty/welcome state. Once the user sends * a message, a session is created and the workbench ChatViewPane takes over. */ -class NewChatWidget extends Disposable { +class NewChatWidget extends Disposable implements IHistoryNavigationWidget { private readonly _targetPicker: SessionTargetPicker; private readonly _isolationModePicker: IsolationModePicker; @@ -93,6 +101,13 @@ class NewChatWidget extends Disposable { private readonly _syncIndicator: SyncIndicator; private readonly _options: INewChatWidgetOptions; + // IHistoryNavigationWidget + private readonly _onDidFocus = this._register(new Emitter()); + readonly onDidFocus = this._onDidFocus.event; + private readonly _onDidBlur = this._register(new Emitter()); + readonly onDidBlur = this._onDidBlur.event; + get element(): HTMLElement { return this._editorContainer; } + // Input private _editor!: CodeEditorWidget; private _editorContainer!: HTMLElement; @@ -137,6 +152,11 @@ class NewChatWidget extends Disposable { // Slash commands private _slashCommandHandler: SlashCommandHandler | undefined; + // Input history + private readonly _history: ChatHistoryNavigator; + private _historyNavigationBackwardsEnablement!: IHistoryNavigationContext['historyNavigationBackwardsEnablement']; + private _historyNavigationForwardsEnablement!: IHistoryNavigationContext['historyNavigationForwardsEnablement']; + constructor( options: INewChatWidgetOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -144,7 +164,6 @@ class NewChatWidget extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IContextMenuService contextMenuService: IContextMenuService, @ILogService private readonly logService: ILogService, @IHoverService private readonly hoverService: IHoverService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -153,11 +172,12 @@ class NewChatWidget extends Disposable { @IStorageService private readonly storageService: IStorageService, ) { super(); + this._history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments)); this._folderPicker = this._register(this.instantiationService.createInstance(FolderPicker)); this._repoPicker = this._register(this.instantiationService.createInstance(RepoPicker)); this._cloudModelPicker = this._register(this.instantiationService.createInstance(CloudModelPicker)); - this._targetPicker = this._register(new SessionTargetPicker(options.allowedTargets, options.defaultTarget)); + this._targetPicker = this._register(new SessionTargetPicker(options.allowedTargets, this._resolveDefaultTarget(options))); this._isolationModePicker = this._register(this.instantiationService.createInstance(IsolationModePicker)); this._branchPicker = this._register(this.instantiationService.createInstance(BranchPicker)); this._syncIndicator = this._register(this.instantiationService.createInstance(SyncIndicator)); @@ -253,6 +273,9 @@ class NewChatWidget extends Disposable { // Initialize model picker this._initDefaultModel(); + // Restore draft input state from storage + this._restoreState(); + // Create initial session this._createNewSession(); @@ -385,6 +408,15 @@ class NewChatWidget extends Disposable { const editorContainer = this._editorContainer = dom.append(container, dom.$('.sessions-chat-editor')); editorContainer.style.height = `${MIN_EDITOR_HEIGHT}px`; + // Create scoped context key service and register history navigation + // BEFORE creating the editor, so the editor's context key scope is a child + const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(container)); + const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this)); + this._historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; + this._historyNavigationForwardsEnablement = historyNavigationForwardsEnablement; + + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService]))); + const uri = URI.from({ scheme: 'sessions-chat', path: `input-${Date.now()}` }); const textModel = this._register(this.modelService.createModel('', null, uri, true)); @@ -419,7 +451,7 @@ class NewChatWidget extends Disposable { ]), }; - this._editor = this._register(this.instantiationService.createInstance( + this._editor = this._register(scopedInstantiationService.createInstance( CodeEditorWidget, editorContainer, editorOptions, widgetOptions, )); this._editor.setModel(textModel); @@ -427,6 +459,9 @@ class NewChatWidget extends Disposable { // Ensure suggest widget renders above the input (not clipped by container) SuggestController.get(this._editor)?.forceRenderingAbove(); + this._register(this._editor.onDidFocusEditorWidget(() => this._onDidFocus.fire())); + this._register(this._editor.onDidBlurEditorWidget(() => this._onDidBlur.fire())); + this._register(this._editor.onKeyDown(e => { if (e.keyCode === KeyCode.Enter && !e.shiftKey && !e.ctrlKey && !e.altKey) { // Don't send if the suggest widget is visible (let it accept the completion) @@ -444,6 +479,19 @@ class NewChatWidget extends Disposable { } })); + // Update history navigation enablement based on cursor position + const updateHistoryNavigationEnablement = () => { + const model = this._editor.getModel(); + const position = this._editor.getPosition(); + if (!model || !position) { + return; + } + this._historyNavigationBackwardsEnablement.set(position.lineNumber === 1 && position.column === 1); + this._historyNavigationForwardsEnablement.set(position.lineNumber === model.getLineCount() && position.column === model.getLineMaxColumn(position.lineNumber)); + }; + this._register(this._editor.onDidChangeCursorPosition(() => updateHistoryNavigationEnablement())); + updateHistoryNavigationEnablement(); + let previousHeight = -1; this._register(this._editor.onDidContentSizeChange(e => { if (!e.contentHeightChanged) { @@ -557,7 +605,6 @@ class NewChatWidget extends Disposable { currentModel: this._currentLanguageModel, setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { this._currentLanguageModel.set(model, undefined); - this.storageService.store(STORAGE_KEY_LAST_MODEL, model.identifier, StorageScope.PROFILE, StorageTarget.MACHINE); this._newSession.value?.setModelId(model.identifier); this._focusEditor(); }, @@ -581,13 +628,10 @@ class NewChatWidget extends Disposable { private _initDefaultModel(): void { const models = this._getAvailableModels(); - const lastModelId = this.storageService.get(STORAGE_KEY_LAST_MODEL, StorageScope.PROFILE); - const lastModel = lastModelId ? models.find(m => m.identifier === lastModelId) : undefined; - if (lastModel) { - this._currentLanguageModel.set(lastModel, undefined); - } else if (models.length > 0) { - this._currentLanguageModel.set(models[0], undefined); - } + const draft = this._getDraftState(); + const lastModelId = draft?.selectedModel?.identifier ?? this._history.values.at(-1)?.selectedModel?.identifier; + const defaultModel = (lastModelId ? models.find(m => m.identifier === lastModelId) : undefined) ?? models[0]; + this._currentLanguageModel.set(defaultModel, undefined); } private _getAvailableModels(): ILanguageModelChatMetadataAndIdentifier[] { @@ -792,6 +836,60 @@ class NewChatWidget extends Disposable { } } + // --- Input History (IHistoryNavigationWidget) --- + + showPreviousValue(): void { + if (this._history.isAtStart()) { + return; + } + const state = this._getInputState(); + if (state.inputText || state.attachments.length) { + this._history.overlay(state); + } + this._navigateHistory(true); + } + + showNextValue(): void { + if (this._history.isAtEnd()) { + return; + } + const state = this._getInputState(); + if (state.inputText || state.attachments.length) { + this._history.overlay(state); + } + this._navigateHistory(false); + } + + private _getInputState(): IChatModelInputState { + return { + inputText: this._editor?.getModel()?.getValue() ?? '', + attachments: [...this._contextAttachments.attachments], + mode: { id: ChatModeKind.Agent, kind: ChatModeKind.Agent }, + selectedModel: this._currentLanguageModel.get(), + selections: this._editor?.getSelections() ?? [], + contrib: {}, + }; + } + + private _navigateHistory(previous: boolean): void { + const entry = previous ? this._history.previous() : this._history.next(); + const inputText = entry?.inputText ?? ''; + if (entry) { + this._editor?.getModel()?.setValue(inputText); + this._contextAttachments.setAttachments(entry.attachments); + } + aria.status(inputText); + if (previous) { + this._editor.setPosition({ lineNumber: 1, column: 1 }); + } else { + const model = this._editor.getModel(); + if (model) { + const lastLine = model.getLineCount(); + this._editor.setPosition({ lineNumber: lastLine, column: model.getLineMaxColumn(lastLine) }); + } + } + } + // --- Send --- private _updateSendButtonState(): void { @@ -828,6 +926,9 @@ class NewChatWidget extends Disposable { this._contextAttachments.attachments.length > 0 ? [...this._contextAttachments.attachments] : undefined ); + this._history.append(this._getInputState()); + this._clearDraftState(); + this._sending = true; this._editor.updateOptions({ readOnly: true }); this._updateSendButtonState(); @@ -872,6 +973,57 @@ class NewChatWidget extends Disposable { } + private _resolveDefaultTarget(options: INewChatWidgetOptions): AgentSessionProviders { + const draft = this._getDraftState(); + if (draft?.target && options.allowedTargets.includes(draft.target)) { + return draft.target; + } + return options.defaultTarget; + } + + private _restoreState(): void { + const draft = this._getDraftState(); + if (draft) { + this._editor?.getModel()?.setValue(draft.inputText); + if (draft.attachments?.length) { + this._contextAttachments.setAttachments(draft.attachments.map(IChatRequestVariableEntry.fromExport)); + } + if (draft.selectedModel) { + const models = this._getAvailableModels(); + const model = models.find(m => m.identifier === draft.selectedModel?.identifier); + if (model) { + this._currentLanguageModel.set(model, undefined); + } + } + } + } + + private _getDraftState(): (IChatModelInputState & { target?: AgentSessionProviders }) | undefined { + const raw = this.storageService.get(STORAGE_KEY_DRAFT_STATE, StorageScope.WORKSPACE); + if (!raw) { + return undefined; + } + try { + return JSON.parse(raw); + } catch { + return undefined; + } + } + + private _clearDraftState(): void { + this.storageService.remove(STORAGE_KEY_DRAFT_STATE, StorageScope.WORKSPACE); + } + + saveState(): void { + const inputState = this._getInputState(); + const state = { + ...inputState, + attachments: inputState.attachments.map(IChatRequestVariableEntry.toExport), + target: this._targetPicker.selectedTarget, + }; + this.storageService.store(STORAGE_KEY_DRAFT_STATE, JSON.stringify(state), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + // --- Layout --- layout(_height: number, _width: number): void { @@ -956,6 +1108,10 @@ export class NewChatViewPane extends ViewPane { this._widget?.focusInput(); } } + + override saveState(): void { + this._widget?.saveState(); + } } // #endregion From abf18a18fc4392403bf04313d9e7c2ea475f9b82 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 14:40:52 -0800 Subject: [PATCH 06/50] Refactor code structure for improved readability and maintainability --- extensions/github/github-0.0.1.tgz | Bin 0 -> 88843 bytes extensions/github/src/extension.ts | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 extensions/github/github-0.0.1.tgz diff --git a/extensions/github/github-0.0.1.tgz b/extensions/github/github-0.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..f702ccc69f80874126c7e179c0036b73019a27e7 GIT binary patch literal 88843 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ⅇUe#Wp-<77dXwk$t+o+!%D4^uDjMOZUU-AzGsSPq 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 K7@3~{s0ILb#p!wg literal 0 HcmV?d00001 diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 17906c57d44f2..44e90ef997aea 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -96,9 +96,12 @@ function initializeGitExtension(context: ExtensionContext, octokitService: Octok const initialize = () => { gitExtension!.activate() .then(extension => { + console.log('[github ext] git extension activated, enabled:', extension.enabled); const onDidChangeGitExtensionEnablement = (enabled: boolean) => { + console.log('[github ext] onDidChangeGitExtensionEnablement:', enabled); if (enabled) { const gitAPI = extension.getAPI(1); + console.log('[github ext] got gitAPI, repositories:', gitAPI.repositories.length); disposables.add(registerCommands(gitAPI)); disposables.add(new GithubCredentialProviderManager(gitAPI)); @@ -122,8 +125,10 @@ function initializeGitExtension(context: ExtensionContext, octokitService: Octok }; if (gitExtension) { + console.log('[github ext] vscode.git extension found, initializing'); initialize(); } else { + console.log('[github ext] vscode.git extension NOT found, waiting...'); const listener = extensions.onDidChange(() => { if (!gitExtension && extensions.getExtension('vscode.git')) { gitExtension = extensions.getExtension('vscode.git'); From 0f0916dcb0bf1905ae6f02e8cebdbc2715f2e3cd Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 26 Feb 2026 23:44:46 +0100 Subject: [PATCH 07/50] fix titlebar part styling (#298109) --- .../browser/parts/media/titlebarpart.css | 23 +++++++++++++---- src/vs/sessions/browser/parts/titlebarPart.ts | 25 ++----------------- .../browser/media/sessionsTitleBarWidget.css | 7 +++--- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/vs/sessions/browser/parts/media/titlebarpart.css b/src/vs/sessions/browser/parts/media/titlebarpart.css index 22273655103c7..6aab26d26318c 100644 --- a/src/vs/sessions/browser/parts/media/titlebarpart.css +++ b/src/vs/sessions/browser/parts/media/titlebarpart.css @@ -3,8 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left { + display: flex; + height: 100%; + align-items: center; + order: 0; + flex-grow: 2; + justify-content: flex-start; +} + /* Left Tool Bar Container */ -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .left-toolbar-container { +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container { display: none; padding-left: 8px; flex-grow: 0; @@ -17,17 +26,17 @@ order: 2; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .left-toolbar-container:not(.has-no-actions) { +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container:not(.has-no-actions) { display: flex; justify-content: center; align-items: center; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .left-toolbar-container .codicon { +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container .codicon { color: inherit; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .left-toolbar-container .monaco-action-bar .action-item { +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container .monaco-action-bar .action-item { display: flex; } @@ -35,7 +44,7 @@ * The contribution swaps the menu item synchronously, but the toolbar * re-render is async, causing a brief flash. Hide the container via * CSS when sidebar is visible (nosidebar class is removed synchronously). */ -.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .titlebar-container > .titlebar-left > .left-toolbar-container { +.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container { display: none !important; } @@ -43,3 +52,7 @@ .agent-sessions-workbench.mac .part.titlebar .window-controls-container { -webkit-app-region: drag; } + +.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .sessions-titlebar-container > .titlebar-left .window-controls-container { + display: none !important; +} diff --git a/src/vs/sessions/browser/parts/titlebarPart.ts b/src/vs/sessions/browser/parts/titlebarPart.ts index 8eab246ab644e..fa99c2e21e3eb 100644 --- a/src/vs/sessions/browser/parts/titlebarPart.ts +++ b/src/vs/sessions/browser/parts/titlebarPart.ts @@ -18,7 +18,7 @@ import { WORKBENCH_BACKGROUND } from '../../../workbench/common/theme.js'; import { chatBarTitleBackground, chatBarTitleForeground } from '../../common/theme.js'; import { isMacintosh, isWeb, isNative, platformLocale } from '../../../base/common/platform.js'; import { Color } from '../../../base/common/color.js'; -import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, getWindow, getWindowId } from '../../../base/browser/dom.js'; +import { EventType, EventHelper, append, $, addDisposableListener, prepend, getWindow, getWindowId } from '../../../base/browser/dom.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { IStorageService } from '../../../platform/storage/common/storage.js'; @@ -132,7 +132,7 @@ export class TitlebarPart extends Part implements ITitlebarPart { protected override createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; - this.rootContainer = append(parent, $('.titlebar-container.has-center')); + this.rootContainer = append(parent, $('.titlebar-container.sessions-titlebar-container.has-center')); // Draggable region prepend(this.rootContainer, $('div.titlebar-drag-region')); @@ -254,8 +254,6 @@ export class TitlebarPart extends Part implements ITitlebarPart { }); } - private lastLayoutDimension: Dimension | undefined; - get hasZoomableElements(): boolean { return true; // sessions titlebar always has command center and toolbar actions } @@ -268,7 +266,6 @@ export class TitlebarPart extends Part implements ITitlebarPart { } override layout(width: number, height: number): void { - this.lastLayoutDimension = new Dimension(width, height); this.updateLayout(); super.layoutContents(width, height); } @@ -281,24 +278,6 @@ export class TitlebarPart extends Part implements ITitlebarPart { const zoomFactor = getZoomFactor(getWindow(this.element)); this.element.style.setProperty('--zoom-factor', zoomFactor.toString()); this.rootContainer.classList.toggle('counter-zoom', this.preventZoom); - - this.updateCenterOffset(); - } - - private updateCenterOffset(): void { - if (!this.centerContent || !this.lastLayoutDimension) { - return; - } - - // Center the command center relative to the viewport. - // The titlebar only covers the right section (sidebar is to the left), - // so we shift the center content left by half the sidebar width - // using a negative margin. - const windowWidth = this.layoutService.mainContainerDimension.width; - const titlebarWidth = this.lastLayoutDimension.width; - const leftOffset = windowWidth - titlebarWidth; - this.centerContent.style.marginLeft = leftOffset > 0 ? `${-leftOffset / 2}px` : ''; - this.centerContent.style.marginRight = leftOffset > 0 ? `${leftOffset / 2}px` : ''; } focus(): void { diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css index 3bd5ae6c0221c..577c5a482f621 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css @@ -20,16 +20,16 @@ overflow: hidden; color: var(--vscode-commandCenter-foreground); gap: 6px; + cursor: default; } /* Session pill - clickable area for session picker */ .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-pill { display: flex; align-items: center; - cursor: pointer; padding: 0 4px; border-radius: 4px; - overflow: hidden; + min-width: 0; } .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-pill:hover { @@ -62,8 +62,9 @@ display: flex; align-items: center; gap: 6px; - overflow: hidden; + min-width: 0; justify-content: center; + cursor: pointer; } /* Kind icon */ From 68f9ece10a5a89c40d696de2e7cbfde94f8975b4 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 14:48:20 -0800 Subject: [PATCH 08/50] add logging for activation in GitHub extension --- extensions/github/github-0.0.1.tgz | Bin 88843 -> 0 bytes extensions/github/src/extension.ts | 1 + 2 files changed, 1 insertion(+) delete mode 100644 extensions/github/github-0.0.1.tgz diff --git a/extensions/github/github-0.0.1.tgz b/extensions/github/github-0.0.1.tgz deleted file mode 100644 index f702ccc69f80874126c7e179c0036b73019a27e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88843 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ⅇUe#Wp-<77dXwk$t+o+!%D4^uDjMOZUU-AzGsSPq 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 K7@3~{s0ILb#p!wg diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 44e90ef997aea..90b382d5f3deb 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -20,6 +20,7 @@ import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDeta import { OctokitService } from './auth.js'; export function activate(context: ExtensionContext): void { + console.log('[github ext] activate() called'); const disposables: Disposable[] = []; context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())); From 9fd457a2cde50af93d6ec792a635e941e811396c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 26 Feb 2026 14:53:15 -0800 Subject: [PATCH 09/50] plugins: fix overly aggressive marketplace querying (#298092) * plugins: fix overly aggressive marketplace querying Now we (correctly) only fetch marketplace data when searching in the agents view. * comments --- .../common/plugins/agentPluginServiceImpl.ts | 26 +-- .../plugins/pluginMarketplaceService.ts | 67 +++++- .../plugins/pluginMarketplaceService.test.ts | 220 +++++++++++++++++- 3 files changed, 285 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index 8da080588ad8b..8a2b48a7cf595 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from '../../../../../base/common/async.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; @@ -34,7 +33,6 @@ import { parseClaudeHooks } from '../promptSyntax/hookClaudeCompat.js'; import { parseCopilotHooks } from '../promptSyntax/hookCompatibility.js'; import { IHookCommand } from '../promptSyntax/hookSchema.js'; import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js'; -import { IPluginInstallService } from './pluginInstallService.js'; import { IMarketplacePlugin, IPluginMarketplaceService } from './pluginMarketplaceService.js'; const COMMAND_FILE_SUFFIX = '.md'; @@ -268,7 +266,6 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IFileService private readonly _fileService: IFileService, - @IPluginInstallService private readonly _pluginInstallService: IPluginInstallService, @IPluginMarketplaceService private readonly _pluginMarketplaceService: IPluginMarketplaceService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IPathService private readonly _pathService: IPathService, @@ -302,8 +299,6 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent const plugins: IAgentPlugin[] = []; const seenPluginUris = new Set(); const config = this._pluginPathsConfig.get(); - // todo: temporary, we should have a dedicated discovery from the marketplace - const marketplacePluginsByInstallUri = await this._getMarketplacePluginsByInstallUri(); for (const [path, enabled] of Object.entries(config)) { if (!path.trim()) { @@ -328,8 +323,9 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent const key = stat.resource.toString(); if (!seenPluginUris.has(key)) { const adapter = await this._detectPluginFormatAdapter(stat.resource); + const fromMarketplace = await this._pluginMarketplaceService.getMarketplacePluginMetadata(stat.resource); seenPluginUris.add(key); - plugins.push(this._toPlugin(stat.resource, path, enabled, adapter, marketplacePluginsByInstallUri.get(key))); + plugins.push(this._toPlugin(stat.resource, path, enabled, adapter, fromMarketplace)); } } } @@ -340,24 +336,6 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent return plugins; } - private async _getMarketplacePluginsByInstallUri(): Promise> { - const result = new Map(); - let marketplacePlugins: readonly IMarketplacePlugin[]; - try { - marketplacePlugins = await this._pluginMarketplaceService.fetchMarketplacePlugins(CancellationToken.None); - } catch (err) { - this._logService.debug('[ConfiguredAgentPluginDiscovery] Failed to fetch marketplace plugins for provenance mapping:', err); - return result; - } - - for (const marketplacePlugin of marketplacePlugins) { - const installUri = this._pluginInstallService.getPluginInstallUri(marketplacePlugin); - result.set(installUri.toString(), marketplacePlugin); - } - - return result; - } - /** * Resolves a plugin path to one or more resource URIs. Absolute paths are * used directly; relative paths are resolved against each workspace folder. diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index 46cf8c7015530..e536a09d199e2 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -8,13 +8,13 @@ import { Event } from '../../../../../base/common/event.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Lazy } from '../../../../../base/common/lazy.js'; import { revive } from '../../../../../base/common/marshalling.js'; -import { joinPath, normalizePath, relativePath } from '../../../../../base/common/resources.js'; +import { isEqualOrParent, joinPath, normalizePath, relativePath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; -import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { asJson, IRequestService } from '../../../../../platform/request/common/request.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { asJson, IRequestService } from '../../../../../platform/request/common/request.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import type { Dto } from '../../../../services/extensions/common/proxyIdentifier.js'; import { ChatConfiguration } from '../constants.js'; @@ -75,6 +75,7 @@ export interface IPluginMarketplaceService { readonly _serviceBrand: undefined; readonly onDidChangeMarketplaces: Event; fetchMarketplacePlugins(token: CancellationToken): Promise; + getMarketplacePluginMetadata(pluginUri: URI): Promise; } /** @@ -294,6 +295,66 @@ export class PluginMarketplaceService implements IPluginMarketplaceService { ); } + async getMarketplacePluginMetadata(pluginUri: URI): Promise { + const configuredRefs = this._configurationService.getValue(ChatConfiguration.PluginMarketplaces) ?? []; + const refs = parseMarketplaceReferences(configuredRefs); + + for (const ref of refs) { + let repoDir: URI; + try { + repoDir = this._pluginRepositoryService.getRepositoryUri(ref); + } catch { + continue; + } + + if (!isEqualOrParent(pluginUri, repoDir)) { + continue; + } + + for (const def of MARKETPLACE_DEFINITIONS) { + const definitionUri = joinPath(repoDir, def.path); + let json: IMarketplaceJson | undefined; + try { + const contents = await this._fileService.readFile(definitionUri); + json = parseJSONC(contents.value.toString()) as IMarketplaceJson | undefined; + } catch { + continue; + } + + if (!json?.plugins || !Array.isArray(json.plugins)) { + continue; + } + + for (const p of json.plugins) { + if (typeof p.name !== 'string' || !p.name) { + continue; + } + + const source = resolvePluginSource(json.metadata?.pluginRoot, p.source ?? ''); + if (source === undefined) { + continue; + } + + const pluginSourceUri = normalizePath(joinPath(repoDir, source)); + if (isEqualOrParent(pluginUri, pluginSourceUri)) { + return { + name: p.name, + description: p.description ?? '', + version: p.version ?? '', + source, + marketplace: ref.displayLabel, + marketplaceReference: ref, + marketplaceType: def.type, + readmeUri: getMarketplaceReadmeFileUri(repoDir, source), + }; + } + } + } + } + + return undefined; + } + private async _fetchFromClonedRepo(reference: IMarketplaceReference, token: CancellationToken): Promise { let repoDir: URI; try { diff --git a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts index 4db8ae689147f..4b42ab0ea040c 100644 --- a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts @@ -4,8 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { joinPath } from '../../../../../../base/common/resources.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { IRequestService } from '../../../../../../platform/request/common/request.js'; +import { IStorageService, InMemoryStorageService } from '../../../../../../platform/storage/common/storage.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { MarketplaceReferenceKind, parseMarketplaceReference, parseMarketplaceReferences } from '../../../common/plugins/pluginMarketplaceService.js'; +import { IAgentPluginRepositoryService } from '../../../common/plugins/agentPluginRepositoryService.js'; +import { ChatConfiguration } from '../../../common/constants.js'; +import { MarketplaceReferenceKind, MarketplaceType, PluginMarketplaceService, parseMarketplaceReference, parseMarketplaceReferences } from '../../../common/plugins/pluginMarketplaceService.js'; suite('PluginMarketplaceService', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -102,3 +114,209 @@ suite('PluginMarketplaceService', () => { assert.strictEqual(parsed[0].canonicalId, 'github:microsoft/vscode'); }); }); + +suite('PluginMarketplaceService - getMarketplacePluginMetadata', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + const repoDir = URI.file('/cache/agentPlugins/github.com/microsoft/plugins'); + const marketplaceRef = parseMarketplaceReference('microsoft/plugins')!; + + function createMarketplaceJson(plugins: object[], metadata?: object): string { + return JSON.stringify({ metadata, plugins }); + } + + function createService(fileContents: Map): PluginMarketplaceService { + const instantiationService = store.add(new TestInstantiationService()); + + const configService = new TestConfigurationService({ + [ChatConfiguration.PluginMarketplaces]: ['microsoft/plugins'], + [ChatConfiguration.PluginsEnabled]: true, + }); + + const fileService = { + readFile: async (uri: URI) => { + const content = fileContents.get(uri.path); + if (content !== undefined) { + return { value: VSBuffer.fromString(content) }; + } + throw new Error('File not found'); + }, + } as unknown as IFileService; + + const repositoryService = { + getRepositoryUri: () => repoDir, + getPluginInstallUri: (plugin: { source: string }) => joinPath(repoDir, plugin.source), + } as unknown as IAgentPluginRepositoryService; + + instantiationService.stub(IConfigurationService, configService); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(IAgentPluginRepositoryService, repositoryService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IRequestService, {} as unknown as IRequestService); + instantiationService.stub(IStorageService, store.add(new InMemoryStorageService())); + + return instantiationService.createInstance(PluginMarketplaceService); + } + + test('returns metadata for a plugin that matches by source', async () => { + const files = new Map(); + files.set( + joinPath(repoDir, '.github/plugin/marketplace.json').path, + createMarketplaceJson([ + { name: 'my-plugin', description: 'A test plugin', version: '2.0.0', source: 'plugins/my-plugin' }, + ]), + ); + + const service = createService(files); + const pluginUri = joinPath(repoDir, 'plugins/my-plugin'); + + const result = await service.getMarketplacePluginMetadata(pluginUri); + + assert.deepStrictEqual(result && { + name: result.name, + description: result.description, + version: result.version, + source: result.source, + marketplace: result.marketplace, + marketplaceType: result.marketplaceType, + }, { + name: 'my-plugin', + description: 'A test plugin', + version: '2.0.0', + source: 'plugins/my-plugin', + marketplace: marketplaceRef.displayLabel, + marketplaceType: MarketplaceType.Copilot, + }); + }); + + test('returns undefined for a URI outside all marketplace repos', async () => { + const files = new Map(); + files.set( + joinPath(repoDir, '.github/plugin/marketplace.json').path, + createMarketplaceJson([ + { name: 'my-plugin', version: '1.0.0', source: 'plugins/my-plugin' }, + ]), + ); + + const service = createService(files); + const unrelatedUri = URI.file('/some/other/path'); + + const result = await service.getMarketplacePluginMetadata(unrelatedUri); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when plugin URI is in repo but no source matches', async () => { + const files = new Map(); + files.set( + joinPath(repoDir, '.github/plugin/marketplace.json').path, + createMarketplaceJson([ + { name: 'my-plugin', version: '1.0.0', source: 'plugins/my-plugin' }, + ]), + ); + + const service = createService(files); + const noMatchUri = joinPath(repoDir, 'plugins/other-plugin'); + + const result = await service.getMarketplacePluginMetadata(noMatchUri); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when no marketplace.json files exist', async () => { + const service = createService(new Map()); + const pluginUri = joinPath(repoDir, 'plugins/my-plugin'); + + const result = await service.getMarketplacePluginMetadata(pluginUri); + assert.strictEqual(result, undefined); + }); + + test('falls back to Claude marketplace.json when Copilot one is missing', async () => { + const files = new Map(); + files.set( + joinPath(repoDir, '.claude-plugin/marketplace.json').path, + createMarketplaceJson([ + { name: 'claude-plugin', version: '3.0.0', source: 'src/claude-plugin' }, + ]), + ); + + const service = createService(files); + const pluginUri = joinPath(repoDir, 'src/claude-plugin'); + + const result = await service.getMarketplacePluginMetadata(pluginUri); + assert.ok(result); + assert.strictEqual(result!.name, 'claude-plugin'); + assert.strictEqual(result!.marketplaceType, MarketplaceType.Claude); + }); + + test('resolves source relative to pluginRoot metadata', async () => { + const files = new Map(); + files.set( + joinPath(repoDir, '.github/plugin/marketplace.json').path, + createMarketplaceJson( + [{ name: 'nested', version: '1.0.0', source: 'my-plugin' }], + { pluginRoot: 'packages' }, + ), + ); + + const service = createService(files); + const pluginUri = joinPath(repoDir, 'packages/my-plugin'); + + const result = await service.getMarketplacePluginMetadata(pluginUri); + assert.ok(result); + assert.strictEqual(result!.name, 'nested'); + assert.strictEqual(result!.source, 'packages/my-plugin'); + }); + + test('selects the correct plugin among multiple entries', async () => { + const files = new Map(); + files.set( + joinPath(repoDir, '.github/plugin/marketplace.json').path, + createMarketplaceJson([ + { name: 'alpha', version: '1.0.0', source: 'plugins/alpha' }, + { name: 'beta', version: '2.0.0', source: 'plugins/beta' }, + { name: 'gamma', version: '3.0.0', source: 'plugins/gamma' }, + ]), + ); + + const service = createService(files); + const pluginUri = joinPath(repoDir, 'plugins/beta'); + + const result = await service.getMarketplacePluginMetadata(pluginUri); + assert.ok(result); + assert.strictEqual(result!.name, 'beta'); + assert.strictEqual(result!.version, '2.0.0'); + }); + + test('returns undefined when no marketplaces are configured', async () => { + const instantiationService = store.add(new TestInstantiationService()); + instantiationService.stub(IConfigurationService, new TestConfigurationService({ + [ChatConfiguration.PluginMarketplaces]: [], + [ChatConfiguration.PluginsEnabled]: true, + })); + instantiationService.stub(IFileService, {} as unknown as IFileService); + instantiationService.stub(IAgentPluginRepositoryService, {} as unknown as IAgentPluginRepositoryService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IRequestService, {} as unknown as IRequestService); + instantiationService.stub(IStorageService, store.add(new InMemoryStorageService())); + + const service = instantiationService.createInstance(PluginMarketplaceService); + const result = await service.getMarketplacePluginMetadata(URI.file('/any/path')); + assert.strictEqual(result, undefined); + }); + + test('matches when pluginUri is a subdirectory inside a plugin source', async () => { + const files = new Map(); + files.set( + joinPath(repoDir, '.github/plugin/marketplace.json').path, + createMarketplaceJson([ + { name: 'my-plugin', version: '1.0.0', source: 'plugins/my-plugin' }, + ]), + ); + + const service = createService(files); + const nestedUri = joinPath(repoDir, 'plugins/my-plugin/src/tool.ts'); + + const result = await service.getMarketplacePluginMetadata(nestedUri); + assert.ok(result); + assert.strictEqual(result!.name, 'my-plugin'); + }); +}); From 88c46908429ff281647f18c7163ac7977d2bcc3c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 26 Feb 2026 14:53:31 -0800 Subject: [PATCH 10/50] debug: bump js-debug to 1.110 (#298111) --- product.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/product.json b/product.json index af053a48660fd..cbd7903d0b441 100644 --- a/product.json +++ b/product.json @@ -52,8 +52,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.105.0", - "sha256": "0c45b90342e8aafd4ff2963b4006de64208ca58c2fd01fea7a710fe61dcfd12a", + "version": "1.110.0", + "sha256": "ad3f7d935b64f4ee123853c464b47b3cba13e5018edef831770ae9c3eb218f5b", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", From 6ce4655c56b83fae8aac217dcae2cafe079453e5 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 14:54:56 -0800 Subject: [PATCH 11/50] enable virtual workspaces capability in package.json --- extensions/github/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index 0e33a21f9186a..bce90fe1812d5 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -22,7 +22,7 @@ "main": "./out/extension.js", "type": "module", "capabilities": { - "virtualWorkspaces": false, + "virtualWorkspaces": true, "untrustedWorkspaces": { "supported": true } From 867b98ab99e852e859c64237de4884a3f6a000e3 Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:56:35 -0800 Subject: [PATCH 12/50] Adding description for Sandboxing related tooltips in mcp.json (#298059) --- src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index e74c8948c3cc2..fce88654401e4 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -185,20 +185,23 @@ export const mcpServerSchema: IJSONSchema = { additionalProperties: false, properties: { sandbox: { - description: localize('app.mcp.json.sandbox', "Default sandbox settings for running servers."), + description: localize('app.mcp.json.sandbox', "Sandbox config that determines file system and network access. Sandboxing is enabled when sandboxEnabled property is set at the server level on Mac OS and Linux only."), type: 'object', additionalProperties: false, properties: { network: { + description: localize('app.mcp.json.sandbox.network', "Network access settings for the sandboxed server."), type: 'object', additionalProperties: false, properties: { allowedDomains: { + description: localize('app.mcp.json.sandbox.network.allowedDomains', "List of domains that the server is allowed to access. Wildcards are supported, e.g. `*.example.com`."), type: 'array', items: { type: 'string' }, default: [] }, deniedDomains: { + description: localize('app.mcp.json.sandbox.network.deniedDomains', "List of domains that the server is not allowed to access. e.g. `invalid.example.com`."), type: 'array', items: { type: 'string' }, default: [] @@ -206,20 +209,24 @@ export const mcpServerSchema: IJSONSchema = { } }, filesystem: { + description: localize('app.mcp.json.sandbox.filesystem', "Filesystem access settings for the sandboxed server. Glob patterns are supported for Mac OS only."), type: 'object', additionalProperties: false, properties: { denyRead: { + description: localize('app.mcp.json.sandbox.filesystem.denyRead', "List of file paths that the server is not allowed to read. By default, all files are allowed to be read. e.g. `~/src/secrets`."), type: 'array', items: { type: 'string' }, default: [] }, allowWrite: { + description: localize('app.mcp.json.sandbox.filesystem.allowWrite', "List of file paths that the server is allowed to write to. e.g. `~/src/`."), type: 'array', items: { type: 'string' }, default: [] }, denyWrite: { + description: localize('app.mcp.json.sandbox.filesystem.denyWrite', "List of file paths that the server is not allowed to write to. e.g. `~/src/auth/`."), type: 'array', items: { type: 'string' }, default: [] From 1a353de913e968f8a1f7231ec013d8258abebf8b Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:56:37 -0800 Subject: [PATCH 13/50] enable terminal dropdowns (#298079) flip terminal dropdowns on for everyone --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index aae88d01f5e86..a73f03cf41b08 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1139,7 +1139,7 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.SimpleTerminalCollapsible]: { type: 'boolean', - default: product.quality !== 'stable', + default: true, markdownDescription: nls.localize('chat.tools.terminal.simpleCollapsible', "When enabled, terminal tool calls are always displayed in a collapsible container with a simplified view."), tags: ['experimental'], }, From 8f0fd7e348a467ded5f481d0edbe440ec96b95d6 Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Fri, 27 Feb 2026 00:02:18 +0100 Subject: [PATCH 14/50] Include ready remote extension hosts in immediate activation (#298114) Include ready remote extension hosts in immediate activation (fix #297019) When `activateByEvent` is called with `ActivationKind.Immediate`, remote extension hosts were unconditionally excluded to avoid blocking during remote authority resolution. This caused extensions that only run on the remote host (e.g. microsoft-authentication in web Codespaces) to never get activated within the 5-second timeout window. Add a synchronous `isReady` property to `IExtensionHostManager` so that `_activateByEvent` can include remote hosts that are already connected while still deferring those that aren't. Not-ready remote hosts continue to be replayed via `_pendingRemoteActivationEvents` after initialization. --- .../common/abstractExtensionService.ts | 8 +++-- .../extensions/common/extensionHostManager.ts | 6 ++++ .../common/extensionHostManagers.ts | 1 + .../common/lazyCreateExtensionHostManager.ts | 4 +++ .../test/browser/extensionService.test.ts | 35 +++++++++++++++++-- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index a0e599cd4b4b1..16ace8d597d8b 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -1024,9 +1024,13 @@ export abstract class AbstractExtensionService extends Disposable implements IEx let managers: IExtensionHostManager[]; if (activationKind === ActivationKind.Immediate) { // For immediate activation, only activate on local extension hosts - // and defer remote activation until the remote host is ready + // and on remote extension hosts that are already ready. + // Defer activation for remote hosts that are not yet ready to avoid + // blocking (e.g. during remote authority resolution). managers = this._extensionHostManagers.filter( - extHostManager => extHostManager.kind === ExtensionHostKind.LocalProcess || extHostManager.kind === ExtensionHostKind.LocalWebWorker + extHostManager => extHostManager.kind === ExtensionHostKind.LocalProcess + || extHostManager.kind === ExtensionHostKind.LocalWebWorker + || extHostManager.isReady ); this._pendingRemoteActivationEvents.add(activationEvent); } else { diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 88e8fd1d2571f..c7e1f8b626194 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -71,6 +71,7 @@ export class ExtensionHostManager extends Disposable implements IExtensionHostMa private readonly _customers: IDisposable[]; private readonly _extensionHost: IExtensionHost; private _proxy: Promise | null; + private _hasStarted: boolean = false; public get pid(): number | null { return this._extensionHost.pid; @@ -152,6 +153,7 @@ export class ExtensionHostManager extends Disposable implements IExtensionHostMa } ); this._proxy.then(() => { + this._hasStarted = true; initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent, ActivationKind.Normal)); this._register(registerLatencyTestProvider({ measure: () => this.measure() @@ -196,6 +198,10 @@ export class ExtensionHostManager extends Disposable implements IExtensionHostMa }; } + public get isReady(): boolean { + return this._hasStarted; + } + public async ready(): Promise { await this._proxy; } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManagers.ts b/src/vs/workbench/services/extensions/common/extensionHostManagers.ts index 6c0947ddb9d0c..8b534771e44c1 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManagers.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManagers.ts @@ -22,6 +22,7 @@ export interface IExtensionHostManager { readonly onDidChangeResponsiveState: Event; disconnect(): Promise; dispose(): void; + readonly isReady: boolean; ready(): Promise; representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean; deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise; diff --git a/src/vs/workbench/services/extensions/common/lazyCreateExtensionHostManager.ts b/src/vs/workbench/services/extensions/common/lazyCreateExtensionHostManager.ts index f9acb2a03c971..8662618b1b94b 100644 --- a/src/vs/workbench/services/extensions/common/lazyCreateExtensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/lazyCreateExtensionHostManager.ts @@ -90,6 +90,10 @@ export class LazyCreateExtensionHostManager extends Disposable implements IExten return actual; } + public get isReady(): boolean { + return this._startCalled.isOpen() && (this._actual?.isReady ?? false); + } + public async ready(): Promise { await this._startCalled.wait(); if (this._actual) { diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index fa3b8370531ed..86c11e9e1bf61 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -192,12 +192,15 @@ suite('ExtensionService', () => { private _extHostId = 0; public readonly order: string[] = []; public readonly activationEvents: { event: string; activationKind: ActivationKind; kind: ExtensionHostKind }[] = []; + public remoteExtHostIsReady = true; + public localExtHostIsReady = true; protected _pickExtensionHostKind(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null { throw new Error('Method not implemented.'); } protected override _doCreateExtensionHostManager(extensionHost: IExtensionHost, initialActivationEvents: string[]): IExtensionHostManager { const order = this.order; const activationEvents = this.activationEvents; + const extService = this; const extensionHostId = ++this._extHostId; const extHostKind = extensionHost.runningLocation.kind; order.push(`create ${extensionHostId}`); @@ -205,6 +208,7 @@ suite('ExtensionService', () => { override onDidExit = Event.None; override onDidChangeResponsiveState = Event.None; override kind = extHostKind; + override get isReady() { return extHostKind === ExtensionHostKind.Remote ? extService.remoteExtHostIsReady : extService.localExtHostIsReady; } override disconnect() { return Promise.resolve(); } @@ -334,16 +338,41 @@ suite('ExtensionService', () => { assert.strictEqual(events[0].activationKind, ActivationKind.Immediate); }); - test('Immediate activation only activates local extension hosts', async () => { + test('Immediate activation includes ready remote extension hosts (issue #297019)', async () => { extService.activationEvents.length = 0; // Clear any initial activations await extService.activateByEvent('onTest', ActivationKind.Immediate); - // Should only activate on local hosts (LocalProcess and LocalWebWorker), not Remote + // When remote host is ready, Immediate activation should include it const activatedKinds = extService.activationEvents.map(e => e.kind); assert.ok(activatedKinds.includes(ExtensionHostKind.LocalProcess), 'Should activate on LocalProcess'); assert.ok(activatedKinds.includes(ExtensionHostKind.LocalWebWorker), 'Should activate on LocalWebWorker'); - assert.ok(!activatedKinds.includes(ExtensionHostKind.Remote), 'Should NOT activate on Remote'); + assert.ok(activatedKinds.includes(ExtensionHostKind.Remote), 'Should activate on ready Remote host'); + }); + + test('Immediate activation excludes not-ready remote extension hosts', async () => { + extService.remoteExtHostIsReady = false; + extService.activationEvents.length = 0; // Clear any initial activations + + await extService.activateByEvent('onTest', ActivationKind.Immediate); + + // When remote host is not ready, Immediate activation should skip it + const activatedKinds = extService.activationEvents.map(e => e.kind); + assert.ok(activatedKinds.includes(ExtensionHostKind.LocalProcess), 'Should activate on LocalProcess'); + assert.ok(activatedKinds.includes(ExtensionHostKind.LocalWebWorker), 'Should activate on LocalWebWorker'); + assert.ok(!activatedKinds.includes(ExtensionHostKind.Remote), 'Should NOT activate on not-ready Remote host'); + }); + + test('Immediate activation still activates local extension hosts even when not ready', async () => { + extService.localExtHostIsReady = false; + extService.activationEvents.length = 0; // Clear any initial activations + + await extService.activateByEvent('onTest', ActivationKind.Immediate); + + // Local hosts should always be activated for Immediate, regardless of isReady + const activatedKinds = extService.activationEvents.map(e => e.kind); + assert.ok(activatedKinds.includes(ExtensionHostKind.LocalProcess), 'Should activate on LocalProcess even when not ready'); + assert.ok(activatedKinds.includes(ExtensionHostKind.LocalWebWorker), 'Should activate on LocalWebWorker even when not ready'); }); test('Normal activation activates all extension hosts', async () => { From c4f26872d1350e7bd6f7a7fc87c7d9cddf4bef5b Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:08:16 -0800 Subject: [PATCH 15/50] Add command to check for open pull requests and refactor session repository resolution --- extensions/github/src/commands.ts | 78 +++++++++++++++---- extensions/github/src/extension.ts | 6 -- .../changesView/browser/changesView.ts | 11 ++- 3 files changed, 74 insertions(+), 21 deletions(-) diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 1ab189feae75a..bbdec6c59a679 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -35,46 +35,94 @@ async function openVscodeDevLink(gitAPI: GitAPI): Promise { - if (!sessionResource || !sessionMetadata?.worktreePath) { - return; +interface ResolvedSessionRepo { + repository: Repository; + remoteInfo: { owner: string; repo: string }; + gitRemote: { name: string; fetchUrl: string }; + head: { name: string; upstream?: { name: string; remote: string; commit: string } }; +} + +function resolveSessionRepo(gitAPI: GitAPI, sessionMetadata: { worktreePath?: string } | undefined, showErrors: boolean): ResolvedSessionRepo | undefined { + if (!sessionMetadata?.worktreePath) { + return undefined; } const worktreeUri = vscode.Uri.file(sessionMetadata.worktreePath); const repository = gitAPI.getRepository(worktreeUri); if (!repository) { - vscode.window.showErrorMessage(vscode.l10n.t('Could not find a git repository for the session worktree.')); - return; + if (showErrors) { + vscode.window.showErrorMessage(vscode.l10n.t('Could not find a git repository for the session worktree.')); + } + return undefined; } - // Find the GitHub remote const remotes = repository.state.remotes .filter(remote => remote.fetchUrl && getRepositoryFromUrl(remote.fetchUrl)); if (remotes.length === 0) { - vscode.window.showErrorMessage(vscode.l10n.t('Could not find a GitHub remote for this repository.')); - return; + if (showErrors) { + vscode.window.showErrorMessage(vscode.l10n.t('Could not find a GitHub remote for this repository.')); + } + return undefined; } - // Prefer upstream -> origin -> first const gitRemote = remotes.find(r => r.name === 'upstream') ?? remotes.find(r => r.name === 'origin') ?? remotes[0]; const remoteInfo = getRepositoryFromUrl(gitRemote.fetchUrl!); if (!remoteInfo) { - vscode.window.showErrorMessage(vscode.l10n.t('Could not parse GitHub remote URL.')); - return; + if (showErrors) { + vscode.window.showErrorMessage(vscode.l10n.t('Could not parse GitHub remote URL.')); + } + return undefined; } - // Get the current branch (the worktree branch) const head = repository.state.HEAD; if (!head?.name) { - vscode.window.showErrorMessage(vscode.l10n.t('Could not determine the current branch.')); + if (showErrors) { + vscode.window.showErrorMessage(vscode.l10n.t('Could not determine the current branch.')); + } + return undefined; + } + + return { repository, remoteInfo, gitRemote: { name: gitRemote.name, fetchUrl: gitRemote.fetchUrl! }, head: head as ResolvedSessionRepo['head'] }; +} + +async function checkOpenPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise { + const resolved = resolveSessionRepo(gitAPI, sessionMetadata, false); + if (!resolved) { return; } + try { + const octokit = await getOctokit(); + const { data: pullRequests } = await octokit.pulls.list({ + owner: resolved.remoteInfo.owner, + repo: resolved.remoteInfo.repo, + head: `${resolved.remoteInfo.owner}:${resolved.head.name}`, + state: 'open', + }); + + vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', pullRequests.length > 0); + } catch { + // Silently fail — leave context key unchanged + } +} + +async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise { + if (!sessionResource) { + return; + } + + const resolved = resolveSessionRepo(gitAPI, sessionMetadata, true); + if (!resolved) { + return; + } + + const { repository, remoteInfo, gitRemote, head } = resolved; + // Ensure the branch is published to the remote if (!head.upstream) { try { @@ -205,5 +253,9 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return createPullRequest(gitAPI, sessionResource, sessionMetadata); })); + disposables.add(vscode.commands.registerCommand('github.checkOpenPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => { + return checkOpenPullRequest(gitAPI, sessionResource, sessionMetadata); + })); + return disposables; } diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 90b382d5f3deb..17906c57d44f2 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -20,7 +20,6 @@ import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDeta import { OctokitService } from './auth.js'; export function activate(context: ExtensionContext): void { - console.log('[github ext] activate() called'); const disposables: Disposable[] = []; context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())); @@ -97,12 +96,9 @@ function initializeGitExtension(context: ExtensionContext, octokitService: Octok const initialize = () => { gitExtension!.activate() .then(extension => { - console.log('[github ext] git extension activated, enabled:', extension.enabled); const onDidChangeGitExtensionEnablement = (enabled: boolean) => { - console.log('[github ext] onDidChangeGitExtensionEnablement:', enabled); if (enabled) { const gitAPI = extension.getAPI(1); - console.log('[github ext] got gitAPI, repositories:', gitAPI.repositories.length); disposables.add(registerCommands(gitAPI)); disposables.add(new GithubCredentialProviderManager(gitAPI)); @@ -126,10 +122,8 @@ function initializeGitExtension(context: ExtensionContext, octokitService: Octok }; if (gitExtension) { - console.log('[github ext] vscode.git extension found, initializing'); initialize(); } else { - console.log('[github ext] vscode.git extension NOT found, waiting...'); const listener = extensions.onDidChange(() => { if (!gitExtension && extensions.getExtension('vscode.git')) { gitExtension = extensions.getExtension('vscode.git'); diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index fd6f40a2d6a02..702acb7808c46 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -54,6 +54,7 @@ import { createFileIconThemableTreeContainerScope } from '../../../../workbench/ import { IActivityService, NumberBadge } from '../../../../workbench/services/activity/common/activity.js'; import { IEditorService, MODAL_GROUP, SIDE_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; @@ -236,6 +237,7 @@ export class ChangesViewPane extends ViewPane { @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, @ILabelService private readonly labelService: ILabelService, @IStorageService private readonly storageService: IStorageService, + @ICommandService private readonly commandService: ICommandService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -546,7 +548,13 @@ export class ChangesViewPane extends ViewPane { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; - console.log('[changesView] isSessionMenu:', isSessionMenu, 'menuId:', menuId.id, 'sessionResource:', sessionResource?.toString()); + + // Proactively check if a PR exists for the current session branch + if (isSessionMenu && sessionResource) { + const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata; + this.commandService.executeCommand('github.checkOpenPullRequest', sessionResource, metadata).catch(() => { /* ignore */ }); + } + reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, this.actionsContainer!, @@ -557,7 +565,6 @@ export class ChangesViewPane extends ViewPane { ? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] } : { shouldForwardArgs: true }, buttonConfigProvider: (action) => { - console.log('[changesView] buttonConfigProvider action:', action.id); if (action.id === 'chatEditing.viewChanges' || action.id === 'chatEditing.viewPreviousEdits' || action.id === 'chatEditing.viewAllSessionChanges' || action.id === 'chat.openSessionWorktreeInVSCode') { const diffStatsLabel = new MarkdownString( `+${added} -${removed}`, From 7822fcf9aaea4405efb9912efd6a9c350b5d14d9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 27 Feb 2026 00:09:52 +0100 Subject: [PATCH 16/50] fix comparing active sessions (#298113) --- .../browser/sessionsManagementService.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index b7a50d1210132..8524549cea332 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -5,7 +5,6 @@ import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { equals } from '../../../../base/common/objects.js'; import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -443,12 +442,13 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } private doSetActiveSession(activeSessionItem: IActiveSessionItem | undefined): void { - if (equals(this._activeSession.get(), activeSessionItem)) { + if (this.equalsSessionItem(this._activeSession.get(), activeSessionItem)) { return; } if (activeSessionItem) { - this.logService.info(`[ActiveSessionService] Active session changed: ${activeSessionItem.resource.toString()}, repository: ${activeSessionItem.repository?.toString() ?? 'none'}`); + this.logService.info(`[ActiveSessionService] Active session changed: ${activeSessionItem.resource.toString()}`); + this.logService.trace(`[ActiveSessionService] Active session details: ${JSON.stringify(activeSessionItem)}`); } else { this.logService.trace('[ActiveSessionService] Active session cleared'); } @@ -456,6 +456,21 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this._activeSession.set(activeSessionItem, undefined); } + private equalsSessionItem(a: IActiveSessionItem | undefined, b: IActiveSessionItem | undefined): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return ( + a.label === b.label && + a.resource.toString() === b.resource.toString() && + a.repository?.toString() === b.repository?.toString() && + a.worktree?.toString() === b.worktree?.toString() + ); + } + async commitWorktreeFiles(session: IActiveSessionItem, fileUris: URI[]): Promise { const worktreeUri = session.worktree; if (!worktreeUri) { From 7c32c58bbda508e49e7bbdfdd387cef66fc72e32 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:09:54 -0800 Subject: [PATCH 17/50] Remove 'state: open' from pull request creation and check functions --- extensions/github/src/commands.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index bbdec6c59a679..879b90ad575e9 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -102,7 +102,6 @@ async function checkOpenPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri owner: resolved.remoteInfo.owner, repo: resolved.remoteInfo.repo, head: `${resolved.remoteInfo.owner}:${resolved.head.name}`, - state: 'open', }); vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', pullRequests.length > 0); @@ -145,7 +144,6 @@ async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | u owner: remoteInfo.owner, repo: remoteInfo.repo, head: `${remoteInfo.owner}:${head.name}`, - state: 'open', }); if (pullRequests.length > 0) { From 19a12a4ff3ea4c6d35a6bbda308645eccfd9bea5 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:16:05 -0800 Subject: [PATCH 18/50] Update pull request state handling to include all states in checks --- extensions/github/src/commands.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 879b90ad575e9..3adf7d3dcae8c 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -98,13 +98,14 @@ async function checkOpenPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri try { const octokit = await getOctokit(); - const { data: pullRequests } = await octokit.pulls.list({ + const { data: openPRs } = await octokit.pulls.list({ owner: resolved.remoteInfo.owner, repo: resolved.remoteInfo.repo, head: `${resolved.remoteInfo.owner}:${resolved.head.name}`, + state: 'all', }); - vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', pullRequests.length > 0); + vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', openPRs.length > 0); } catch { // Silently fail — leave context key unchanged } @@ -144,6 +145,7 @@ async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | u owner: remoteInfo.owner, repo: remoteInfo.repo, head: `${remoteInfo.owner}:${head.name}`, + state: 'all', }); if (pullRequests.length > 0) { From c040dd4b136a4f26a7b0297bcb35989b15cadbfc Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:18:43 -0800 Subject: [PATCH 19/50] Refactor pull request handling to open repository page on API failure and update command to open pull request --- extensions/github/src/commands.ts | 33 ++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 3adf7d3dcae8c..f6af73536ef70 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -138,30 +138,39 @@ async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | u } } - // Check if a PR already exists for this branch + // Build the GitHub PR creation URL + // Format: https://github.com/owner/repo/compare/base...head + const prUrl = `https://github.com/${remoteInfo.owner}/${remoteInfo.repo}/compare/${head.name}?expand=1`; + + vscode.env.openExternal(vscode.Uri.parse(prUrl)); +} + +async function openPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise { + const resolved = resolveSessionRepo(gitAPI, sessionMetadata, true); + if (!resolved) { + return; + } + try { const octokit = await getOctokit(); const { data: pullRequests } = await octokit.pulls.list({ - owner: remoteInfo.owner, - repo: remoteInfo.repo, - head: `${remoteInfo.owner}:${head.name}`, + owner: resolved.remoteInfo.owner, + repo: resolved.remoteInfo.repo, + head: `${resolved.remoteInfo.owner}:${resolved.head.name}`, state: 'all', }); if (pullRequests.length > 0) { - vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', true); vscode.env.openExternal(vscode.Uri.parse(pullRequests[0].html_url)); return; } } catch { - // If the API call fails, fall through to open the creation URL + // If the API call fails, fall through to open the repo page } - // Build the GitHub PR creation URL - // Format: https://github.com/owner/repo/compare/base...head - const prUrl = `https://github.com/${remoteInfo.owner}/${remoteInfo.repo}/compare/${head.name}?expand=1`; - - vscode.env.openExternal(vscode.Uri.parse(prUrl)); + // Fallback: open the repository page + const { remoteInfo } = resolved; + vscode.env.openExternal(vscode.Uri.parse(`https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`)); } async function openOnGitHub(repository: Repository, commit: string): Promise { @@ -250,7 +259,7 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { })); disposables.add(vscode.commands.registerCommand('github.openPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => { - return createPullRequest(gitAPI, sessionResource, sessionMetadata); + return openPullRequest(gitAPI, sessionResource, sessionMetadata); })); disposables.add(vscode.commands.registerCommand('github.checkOpenPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => { From 6b6f8a7946c6ddf60fecb6d9ee3c302f50c2caad Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:24:02 -0800 Subject: [PATCH 20/50] Add custom label for pull request actions in ChangesViewPane --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 702acb7808c46..b2dff5047e432 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -573,7 +573,7 @@ export class ChangesViewPane extends ViewPane { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') { - return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' }; + return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow', customLabel: localize('pullRequest.short', "PR") }; } if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; From 513e8759b97762f9ab2206b46ce8da80b1e5789d Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:28:15 -0800 Subject: [PATCH 21/50] Add dynamic button label adaptation based on container width in ChangesViewPane --- .../contrib/changesView/browser/changesView.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index b2dff5047e432..9b21cab211df3 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -544,10 +544,22 @@ export class ChangesViewPane extends ViewPane { return files > 0; })); + // Track container width to adapt button labels + const actionsContainerWidth = observableValue(this, this.actionsContainer.clientWidth); + const resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + actionsContainerWidth.set(entry.contentRect.width, undefined); + } + }); + resizeObserver.observe(this.actionsContainer); + this.renderDisposables.add({ dispose: () => resizeObserver.disconnect() }); + this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); + const containerWidth = actionsContainerWidth.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; + const useShortPrLabel = containerWidth < 350; // Proactively check if a PR exists for the current session branch if (isSessionMenu && sessionResource) { @@ -573,7 +585,7 @@ export class ChangesViewPane extends ViewPane { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') { - return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow', customLabel: localize('pullRequest.short', "PR") }; + return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow', customLabel: useShortPrLabel ? localize('pullRequest.short', "PR") : undefined }; } if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; From fded7dfe0cd4e234b00ccbf20983b28522ab5782 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:39:25 -0800 Subject: [PATCH 22/50] Adjust PR button short label threshold to 150px Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 9b21cab211df3..375465cecd3d0 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -559,7 +559,7 @@ export class ChangesViewPane extends ViewPane { const sessionResource = activeSessionResource.read(reader); const containerWidth = actionsContainerWidth.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; - const useShortPrLabel = containerWidth < 350; + const useShortPrLabel = containerWidth < 150; // Proactively check if a PR exists for the current session branch if (isSessionMenu && sessionResource) { From fe82a76fecf3efc81a970fd7610813a43c9859a7 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:41:21 -0800 Subject: [PATCH 23/50] thinking header enforce first word must be past tense, dim text after verb (#298119) * thinking headers: dim text after first word on finalize When a thinking header finalizes, everything past the first word (assumed to be a past tense verb) renders at 0.7 opacity while the verb itself stays at full opacity. * thinking titles: enforce first word must be a past tense verb * thinking: remove top gap before first list item * thinking: normalize streaming title detail line-height * thinking: avoid inline-baseline header height jitter * fix jump --- .../chatThinkingContentPart.ts | 43 +++++++++++++++---- .../media/chatThinkingContent.css | 10 ++++- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index f5865ba459266..4a99a9d93bfac 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -583,13 +583,41 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } } + private setFinalizedTitle(title: string): void { + if (!this._collapseButton) { + return; + } + + const labelElement = this._collapseButton.labelElement; + labelElement.textContent = ''; + + const firstSpaceIndex = title.indexOf(' '); + if (firstSpaceIndex === -1) { + // Single word title, no need to split + labelElement.textContent = title; + } else { + const verb = title.substring(0, firstSpaceIndex); + const rest = title.substring(firstSpaceIndex); + + const verbSpan = $('span'); + verbSpan.textContent = verb; + labelElement.appendChild(verbSpan); + + const restSpan = $('span.chat-thinking-title-detail-text'); + restSpan.textContent = rest; + labelElement.appendChild(restSpan); + } + + this._collapseButton.element.ariaLabel = title; + } + private setDropdownClickable(clickable: boolean): void { if (this._collapseButton) { this._collapseButton.element.style.pointerEvents = clickable ? 'auto' : 'none'; } if (!clickable && this.streamingCompleted) { - super.setTitle(this.lastExtractedTitle ?? this.currentTitle); + this.setFinalizedTitle(this.lastExtractedTitle ?? this.currentTitle); } } @@ -745,7 +773,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen if (this.content.generatedTitle) { this.currentTitle = this.content.generatedTitle; - super.setTitle(this.content.generatedTitle); + this.setFinalizedTitle(this.content.generatedTitle); return; } @@ -755,7 +783,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.currentTitle = existingTitle; this.content.generatedTitle = existingTitle; this.setGeneratedTitleOnAllParts(existingTitle); - super.setTitle(existingTitle); + this.setFinalizedTitle(existingTitle); return; } @@ -787,7 +815,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.currentTitle = title; this.content.generatedTitle = title; this.setGeneratedTitleOnAllParts(title); - super.setTitle(title); + this.setFinalizedTitle(title); return; } @@ -837,6 +865,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen OUTPUT FORMAT: - MUST be a single sentence - MUST be under 10 words + - The FIRST word MUST be a past tense verb (e.g. "Updated", "Reviewed", "Created", "Searched", "Analyzed") - No quotes, no trailing punctuation GENERAL: @@ -962,9 +991,7 @@ ${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks): if (generatedTitle && !this._store.isDisposed) { this.currentTitle = generatedTitle; - if (this._collapseButton) { - this._collapseButton.label = generatedTitle; - } + this.setFinalizedTitle(generatedTitle); this.content.generatedTitle = generatedTitle; this.setGeneratedTitleOnAllParts(generatedTitle); return; @@ -1018,7 +1045,7 @@ ${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks): if (this._collapseButton) { this._collapseButton.icon = Codicon.check; - this._collapseButton.label = finalLabel; + this.setFinalizedTitle(finalLabel); } this.updateDropdownClickability(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 1702eaf25b327..9cc43bb069933 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -18,6 +18,8 @@ } > .chat-used-context-label .monaco-button.monaco-icon-button { + display: flex; + align-items: center; line-height: 1.5em; font-size: 13px; @@ -35,11 +37,18 @@ .rendered-markdown.chat-thinking-title-detail { display: inline; + font-size: inherit; + line-height: inherit; > p { display: inline; + margin: 0; } } + + .chat-thinking-title-detail-text { + opacity: 0.7; + } } &.chat-thinking-active > .chat-used-context-label .monaco-button.monaco-icon-button { @@ -160,7 +169,6 @@ padding: 6px 12px 6px 24px; position: relative; font-size: var(--vscode-chat-font-size-body-s); - .progress-container { margin-bottom: 0px; padding-top: 0px; From 7e503a95886b9965f0364a600f606008401d658e Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:42:57 -0800 Subject: [PATCH 24/50] Add check for existing PR on active session change in ChangesViewPane --- .../contrib/changesView/browser/changesView.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 375465cecd3d0..2939226504f57 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -554,6 +554,15 @@ export class ChangesViewPane extends ViewPane { resizeObserver.observe(this.actionsContainer); this.renderDisposables.add({ dispose: () => resizeObserver.disconnect() }); + // Check if a PR exists when the active session changes + this.renderDisposables.add(autorun(reader => { + const sessionResource = activeSessionResource.read(reader); + if (sessionResource) { + const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata; + this.commandService.executeCommand('github.checkOpenPullRequest', sessionResource, metadata).catch(() => { /* ignore */ }); + } + })); + this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); @@ -561,12 +570,6 @@ export class ChangesViewPane extends ViewPane { const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; const useShortPrLabel = containerWidth < 150; - // Proactively check if a PR exists for the current session branch - if (isSessionMenu && sessionResource) { - const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata; - this.commandService.executeCommand('github.checkOpenPullRequest', sessionResource, metadata).catch(() => { /* ignore */ }); - } - reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, this.actionsContainer!, From bdc3d86e3755369cb41643ca55a96e7ce6052c2f Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:45:18 -0800 Subject: [PATCH 25/50] Fix button label adaptation by tracking body container width instead of actions container --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 2939226504f57..7d80242eedc8f 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -545,13 +545,13 @@ export class ChangesViewPane extends ViewPane { })); // Track container width to adapt button labels - const actionsContainerWidth = observableValue(this, this.actionsContainer.clientWidth); + const actionsContainerWidth = observableValue(this, this.bodyContainer.clientWidth); const resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { actionsContainerWidth.set(entry.contentRect.width, undefined); } }); - resizeObserver.observe(this.actionsContainer); + resizeObserver.observe(this.bodyContainer); this.renderDisposables.add({ dispose: () => resizeObserver.disconnect() }); // Check if a PR exists when the active session changes From 4ce5eb1a83cd7f4cff92d27a9a79cd290694d3ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:47:44 -0800 Subject: [PATCH 26/50] Bump hono from 4.12.0 to 4.12.3 in /test/mcp (#298076) Bumps [hono](https://github.com/honojs/hono) from 4.12.0 to 4.12.3. - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.12.0...v4.12.3) --- updated-dependencies: - dependency-name: hono dependency-version: 4.12.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/mcp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index f3a0ceb0bfffd..6e7624dc0da22 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -702,9 +702,9 @@ } }, "node_modules/hono": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz", - "integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", "license": "MIT", "engines": { "node": ">=16.9.0" From 7b33d47bafd50a20f604c1dccf5ac33723078292 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:48:01 -0800 Subject: [PATCH 27/50] Bump minimatch from 10.0.3 to 10.2.4 in /extensions/html-language-features (#298010) Bump minimatch in /extensions/html-language-features Bumps [minimatch](https://github.com/isaacs/minimatch) from 10.0.3 to 10.2.4. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v10.0.3...v10.2.4) --- updated-dependencies: - dependency-name: minimatch dependency-version: 10.2.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../html-language-features/package-lock.json | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/extensions/html-language-features/package-lock.json b/extensions/html-language-features/package-lock.json index 1e97c143679e7..ef75469b9646a 100644 --- a/extensions/html-language-features/package-lock.json +++ b/extensions/html-language-features/package-lock.json @@ -20,27 +20,6 @@ "vscode": "^1.77.0" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@microsoft/1ds-core-js": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", @@ -189,16 +168,37 @@ "vscode": "^1.75.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "license": "ISC", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" From cfc0b71fbf6140e7b41d1be980482a2c59021113 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:48:15 -0800 Subject: [PATCH 28/50] Bump actions/setup-node from 4 to 6 (#297974) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/screenshot-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index 9907b0ccee876..45136b1f8d207 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -29,7 +29,7 @@ jobs: lfs: true - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc From b28e79dfa44b7a93c4e757bf93a4e9a545d359ba Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:48:17 -0800 Subject: [PATCH 29/50] Refactor button label adaptation to track body width using observable value --- .../changesView/browser/changesView.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 7d80242eedc8f..f0f69e06a3eea 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -191,7 +191,7 @@ export class ChangesViewPane extends ViewPane { // Track current body dimensions for list layout private currentBodyHeight = 0; - private currentBodyWidth = 0; + private readonly bodyWidthObs = observableValue(this, 0); // View mode (list vs tree) private readonly viewModeObs: ReturnType>; @@ -544,16 +544,6 @@ export class ChangesViewPane extends ViewPane { return files > 0; })); - // Track container width to adapt button labels - const actionsContainerWidth = observableValue(this, this.bodyContainer.clientWidth); - const resizeObserver = new ResizeObserver(entries => { - for (const entry of entries) { - actionsContainerWidth.set(entry.contentRect.width, undefined); - } - }); - resizeObserver.observe(this.bodyContainer); - this.renderDisposables.add({ dispose: () => resizeObserver.disconnect() }); - // Check if a PR exists when the active session changes this.renderDisposables.add(autorun(reader => { const sessionResource = activeSessionResource.read(reader); @@ -566,9 +556,9 @@ export class ChangesViewPane extends ViewPane { this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); - const containerWidth = actionsContainerWidth.read(reader); + const bodyWidth = this.bodyWidthObs.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; - const useShortPrLabel = containerWidth < 150; + const useShortPrLabel = bodyWidth < 150; reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, @@ -789,14 +779,14 @@ export class ChangesViewPane extends ViewPane { const contentHeight = this.tree.contentHeight; const treeHeight = Math.min(availableHeight, contentHeight); - this.tree.layout(treeHeight, this.currentBodyWidth); + this.tree.layout(treeHeight, this.bodyWidthObs.get()); this.tree.getHTMLElement().style.height = `${treeHeight}px`; } protected override layoutBody(height: number, width: number): void { super.layoutBody(height, width); this.currentBodyHeight = height; - this.currentBodyWidth = width; + this.bodyWidthObs.set(width, undefined); this.layoutTree(); } From e35ef9ba2bda0fe0add3b4c1e042a1072589bf55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:49:33 -0800 Subject: [PATCH 30/50] Bump rollup from 4.57.1 to 4.59.0 in /build/vite (#297821) Bumps [rollup](https://github.com/rollup/rollup) from 4.57.1 to 4.59.0. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v4.57.1...v4.59.0) --- updated-dependencies: - dependency-name: rollup dependency-version: 4.59.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/vite/package-lock.json | 206 +++++++++++++++++------------------ 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index 8d3f50df39b52..de462673a18b1 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -315,9 +315,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -329,9 +329,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -343,9 +343,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -357,9 +357,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -371,9 +371,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -385,9 +385,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -399,9 +399,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -427,9 +427,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -441,9 +441,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -455,9 +455,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -469,9 +469,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -483,9 +483,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -497,9 +497,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -511,9 +511,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -525,9 +525,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -539,9 +539,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -553,9 +553,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -567,9 +567,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -581,9 +581,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -595,9 +595,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -609,9 +609,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -623,9 +623,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -637,9 +637,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -651,9 +651,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1169,9 +1169,9 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -1185,31 +1185,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, From c2c56dfd597960b887fcf0095c668ea68858728b Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:52:24 -0800 Subject: [PATCH 31/50] Refactor ChangesViewPane to track current body width directly instead of using observable value --- .../contrib/changesView/browser/changesView.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index f0f69e06a3eea..97724233f95d9 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -191,7 +191,7 @@ export class ChangesViewPane extends ViewPane { // Track current body dimensions for list layout private currentBodyHeight = 0; - private readonly bodyWidthObs = observableValue(this, 0); + private currentBodyWidth = 0; // View mode (list vs tree) private readonly viewModeObs: ReturnType>; @@ -556,9 +556,7 @@ export class ChangesViewPane extends ViewPane { this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); - const bodyWidth = this.bodyWidthObs.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; - const useShortPrLabel = bodyWidth < 150; reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, @@ -569,7 +567,7 @@ export class ChangesViewPane extends ViewPane { menuOptions: isSessionMenu && sessionResource ? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] } : { shouldForwardArgs: true }, - buttonConfigProvider: (action) => { + buttonConfigProvider: (action, index) => { if (action.id === 'chatEditing.viewChanges' || action.id === 'chatEditing.viewPreviousEdits' || action.id === 'chatEditing.viewAllSessionChanges' || action.id === 'chat.openSessionWorktreeInVSCode') { const diffStatsLabel = new MarkdownString( `+${added} -${removed}`, @@ -578,7 +576,9 @@ export class ChangesViewPane extends ViewPane { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') { - return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow', customLabel: useShortPrLabel ? localize('pullRequest.short', "PR") : undefined }; + // Use short label when there are other buttons alongside the PR button + const useShortLabel = index > 0; + return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow', customLabel: useShortLabel ? localize('pullRequest.short', "PR") : undefined }; } if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; @@ -779,14 +779,14 @@ export class ChangesViewPane extends ViewPane { const contentHeight = this.tree.contentHeight; const treeHeight = Math.min(availableHeight, contentHeight); - this.tree.layout(treeHeight, this.bodyWidthObs.get()); + this.tree.layout(treeHeight, this.currentBodyWidth); this.tree.getHTMLElement().style.height = `${treeHeight}px`; } protected override layoutBody(height: number, width: number): void { super.layoutBody(height, width); this.currentBodyHeight = height; - this.bodyWidthObs.set(width, undefined); + this.currentBodyWidth = width; this.layoutTree(); } From 6bd6a8262b36365970837b9065ce9b4b4bd7912a Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:54:03 -0800 Subject: [PATCH 32/50] Enhance PR button label adaptation to use short label based on action overflow --- .../contrib/changesView/browser/changesView.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 97724233f95d9..2974c477ae527 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -553,9 +553,11 @@ export class ChangesViewPane extends ViewPane { } })); + const prLabelShort = observableValue(this, false); this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); + const useShortPrLabel = prLabelShort.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; reader.store.add(scopedInstantiationService.createInstance( @@ -567,7 +569,7 @@ export class ChangesViewPane extends ViewPane { menuOptions: isSessionMenu && sessionResource ? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] } : { shouldForwardArgs: true }, - buttonConfigProvider: (action, index) => { + buttonConfigProvider: (action) => { if (action.id === 'chatEditing.viewChanges' || action.id === 'chatEditing.viewPreviousEdits' || action.id === 'chatEditing.viewAllSessionChanges' || action.id === 'chat.openSessionWorktreeInVSCode') { const diffStatsLabel = new MarkdownString( `+${added} -${removed}`, @@ -576,9 +578,7 @@ export class ChangesViewPane extends ViewPane { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') { - // Use short label when there are other buttons alongside the PR button - const useShortLabel = index > 0; - return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow', customLabel: useShortLabel ? localize('pullRequest.short', "PR") : undefined }; + return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow', customLabel: useShortPrLabel ? localize('pullRequest.short', "PR") : undefined }; } if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; @@ -587,6 +587,16 @@ export class ChangesViewPane extends ViewPane { } } )); + + // After layout, check if the actions overflow and switch to short PR label + if (!useShortPrLabel) { + requestAnimationFrame(() => { + const container = this.actionsContainer!; + if (container.scrollWidth > container.clientWidth) { + prLabelShort.set(true, undefined); + } + }); + } })); } From b034e6fe18118fd25ff73bf11905d4e0d73644b7 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:59:18 -0800 Subject: [PATCH 33/50] Add overflow hidden to chat editing session actions container --- .../sessions/contrib/changesView/browser/media/changesView.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/sessions/contrib/changesView/browser/media/changesView.css b/src/vs/sessions/contrib/changesView/browser/media/changesView.css index 1300b886cbcd8..21b6d7dc52661 100644 --- a/src/vs/sessions/contrib/changesView/browser/media/changesView.css +++ b/src/vs/sessions/contrib/changesView/browser/media/changesView.css @@ -98,6 +98,7 @@ .changes-view-body .chat-editing-session-actions.outside-card { margin-bottom: 8px; justify-content: flex-end; + overflow: hidden; } /* Larger action buttons matching SCM ActionButton style */ From bdfa3333cf0af22727264b72303370cf5609a2d1 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 27 Feb 2026 10:59:53 +1100 Subject: [PATCH 34/50] feat(chat): add target property to slash commands and enhance command filtering (#298123) * feat(chat): add target property to slash commands and enhance command filtering * Remove duplicate line * Improve readability * Update src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/chatSlashCommands.ts | 27 +++++--- .../input/editor/chatInputCompletions.ts | 63 ++++++++++++++----- .../common/participants/chatSlashCommands.ts | 2 + 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 0ccf0f81f3ab1..9acdbe8df1642 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -27,6 +27,7 @@ import { CONFIGURE_PROMPTS_ACTION_ID } from './promptSyntax/runPromptAction.js'; import { CONFIGURE_SKILLS_ACTION_ID } from './promptSyntax/skillActions.js'; import { globalAutoApproveDescription } from './tools/languageModelToolsService.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; +import { Target } from '../common/promptSyntax/service/promptsService.js'; export class ChatSlashCommandsContribution extends Disposable { @@ -81,7 +82,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_tools', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async () => { await commandService.executeCommand(ConfigureToolsAction.ID); })); @@ -91,7 +93,7 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_debug', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], }, async () => { await commandService.executeCommand('github.copilot.debug.showChatLogView'); })); @@ -141,7 +143,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z2_fork', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async (_prompt, _progress, _history, _location, sessionResource) => { await commandService.executeCommand('workbench.action.chat.forkConversation', sessionResource); })); @@ -151,7 +154,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z2_rename', executeImmediately: false, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async (prompt, _progress, _history, _location, sessionResource) => { const title = prompt.trim(); if (title) { @@ -216,7 +220,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z1_autoApprove', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, handleEnableAutoApprove)); this._store.add(slashCommandService.registerSlashCommand({ command: 'disableAutoApprove', @@ -224,7 +229,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z1_disableAutoApprove', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, handleDisableAutoApprove)); this._store.add(slashCommandService.registerSlashCommand({ command: 'yolo', @@ -232,7 +238,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z1_yolo', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, handleEnableAutoApprove)); this._store.add(slashCommandService.registerSlashCommand({ command: 'disableYolo', @@ -240,7 +247,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z1_disableYolo', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, handleDisableAutoApprove)); this._store.add(slashCommandService.registerSlashCommand({ command: 'help', @@ -248,7 +256,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z1_help', executeImmediately: true, locations: [ChatAgentLocation.Chat], - modes: [ChatModeKind.Ask] + modes: [ChatModeKind.Ask], + target: Target.VSCode }, async (prompt, progress, _history, _location, sessionResource) => { const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat); const agents = chatAgentService.getAgents(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index e86cda4d36cda..3cd4ffadcb57b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -54,11 +54,12 @@ import { IDynamicVariable } from '../../../../common/attachments/chatVariables.j import { ChatAgentLocation, ChatModeKind, isSupportedChatFileScheme } from '../../../../common/constants.js'; import { isToolSet } from '../../../../common/tools/languageModelToolsService.js'; import { IChatSessionsService } from '../../../../common/chatSessionsService.js'; -import { IPromptsService } from '../../../../common/promptSyntax/service/promptsService.js'; +import { IPromptsService, Target } from '../../../../common/promptSyntax/service/promptsService.js'; import { ChatSubmitAction, IChatExecuteActionContext } from '../../../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../../../chat.js'; import { resizeImage } from '../../../chatImageUtils.js'; import { ChatDynamicVariableModel } from '../../../attachments/chatDynamicVariables.js'; +import { IChatService } from '../../../../common/chatService/chatService.js'; /** * Regex matching a slash command word (e.g. `/foo`). Uses `\p{L}` for Unicode @@ -77,6 +78,8 @@ class SlashCommandCompletions extends Disposable { @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IPromptsService private readonly promptsService: IPromptsService, + @IChatService chatService: IChatService, + @IChatSessionsService chatSessionsService: IChatSessionsService, @IMcpService mcpService: IMcpService, ) { super(); @@ -90,8 +93,15 @@ class SlashCommandCompletions extends Disposable { return null; } - if (widget.lockedAgentId && !widget.attachmentCapabilities.supportsPromptAttachments) { - return null; + + let customAgentTarget: Target | undefined = undefined; + if (widget.lockedAgentId) { + if (!widget.attachmentCapabilities.supportsPromptAttachments) { + return null; + } + const sessionResource = widget.viewModel.model.sessionResource; + const ctx = sessionResource && chatService.getChatSessionFromInternalUri(sessionResource); + customAgentTarget = (ctx ? chatSessionsService.getCustomAgentTargetForSessionType(ctx.chatSessionType) : undefined) ?? Target.Undefined; } const range = computeCompletionRanges(model, position, SlashCommandWord); @@ -117,18 +127,31 @@ class SlashCommandCompletions extends Disposable { } return { - suggestions: slashCommands.map((c, i): CompletionItem => { - const withSlash = `/${c.command}`; - return { - label: withSlash, - insertText: c.executeImmediately ? '' : `${withSlash} `, - documentation: c.detail, - range, - sortText: c.sortText ?? 'a'.repeat(i + 1), - kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` } satisfies IChatExecuteActionContext] } : undefined, - }; - }) + suggestions: slashCommands + .filter(c => { + if (!widget.lockedAgentId) { + return true; + } + if (c.modes && !c.modes.includes(ChatModeKind.Agent)) { + return false; + } + if (c.target && customAgentTarget && c.target !== customAgentTarget) { + return false; + } + return true; + }) + .map((c, i): CompletionItem => { + const withSlash = `/${c.command}`; + return { + label: withSlash, + insertText: c.executeImmediately ? '' : `${withSlash} `, + documentation: c.detail, + range, + sortText: c.sortText ?? 'a'.repeat(i + 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway, + command: c.executeImmediately ? { id: ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` } satisfies IChatExecuteActionContext] } : undefined, + }; + }) }; } })); @@ -213,7 +236,15 @@ class SlashCommandCompletions extends Disposable { } // Filter out commands that are not user-invocable (hidden from / menu) - const userInvocableCommands = promptCommands.filter(c => c.parsedPromptFile?.header?.userInvocable !== false); + const userInvocableCommands = promptCommands + .filter(c => { + // Exclude extension-provided prompt files for locked agents. + if (widget.lockedAgentId && c.promptPath.extension) { + return false; + } + return true; + }) + .filter(c => c.parsedPromptFile?.header?.userInvocable !== false); if (userInvocableCommands.length === 0) { return null; } diff --git a/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts index 9964cfaa4d27e..50f145ccf1004 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts @@ -13,6 +13,7 @@ import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ChatAgentLocation, ChatModeKind } from '../constants.js'; import { URI } from '../../../../../base/common/uri.js'; +import { Target } from '../promptSyntax/service/promptsService.js'; //#region slash service, commands etc @@ -37,6 +38,7 @@ export interface IChatSlashData { locations: ChatAgentLocation[]; modes?: ChatModeKind[]; + target?: Target; } export interface IChatSlashFragment { From 78633e8aef8e8af01d584453863d2b02a9c04ac4 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 16:18:57 -0800 Subject: [PATCH 35/50] Update PR button label adaptation to check button width for overflow --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 2974c477ae527..5b40ac0a4fb4f 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -588,11 +588,11 @@ export class ChangesViewPane extends ViewPane { } )); - // After layout, check if the actions overflow and switch to short PR label + // Check if the PR button is too narrow and switch to short label if (!useShortPrLabel) { requestAnimationFrame(() => { - const container = this.actionsContainer!; - if (container.scrollWidth > container.clientWidth) { + const prButton = this.actionsContainer?.querySelector('.flex-grow') as HTMLElement | null; + if (prButton && prButton.clientWidth < 150) { prLabelShort.set(true, undefined); } }); From 39a9c47c0a0cda81ce2d6733847b4552e489df28 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 16:21:33 -0800 Subject: [PATCH 36/50] Refactor PR button label handling to always render full label and shorten based on width after layout --- .../changesView/browser/changesView.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 5b40ac0a4fb4f..f3de8e1f3b62b 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -553,13 +553,12 @@ export class ChangesViewPane extends ViewPane { } })); - const prLabelShort = observableValue(this, false); this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); - const useShortPrLabel = prLabelShort.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; + // Always render with full label first reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, this.actionsContainer!, @@ -578,7 +577,7 @@ export class ChangesViewPane extends ViewPane { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') { - return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow', customLabel: useShortPrLabel ? localize('pullRequest.short', "PR") : undefined }; + return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' }; } if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; @@ -588,15 +587,16 @@ export class ChangesViewPane extends ViewPane { } )); - // Check if the PR button is too narrow and switch to short label - if (!useShortPrLabel) { - requestAnimationFrame(() => { - const prButton = this.actionsContainer?.querySelector('.flex-grow') as HTMLElement | null; - if (prButton && prButton.clientWidth < 150) { - prLabelShort.set(true, undefined); + // After layout, shorten the PR button label if it's too narrow + requestAnimationFrame(() => { + const prButton = this.actionsContainer?.querySelector('.flex-grow') as HTMLElement | null; + if (prButton && prButton.clientWidth < 150) { + const labelSpan = prButton.querySelector('span:not(.codicon)') as HTMLElement | null; + if (labelSpan) { + labelSpan.textContent = localize('pullRequest.short', "PR"); } - }); - } + } + }); })); } From da914a8e85f476a7842f5086b8fc2273beae48cf Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 16:22:50 -0800 Subject: [PATCH 37/50] Update PR button label adaptation to shorten based on text overflow --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index f3de8e1f3b62b..563486efd703a 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -587,10 +587,10 @@ export class ChangesViewPane extends ViewPane { } )); - // After layout, shorten the PR button label if it's too narrow + // After layout, shorten the PR button label if its text overflows requestAnimationFrame(() => { const prButton = this.actionsContainer?.querySelector('.flex-grow') as HTMLElement | null; - if (prButton && prButton.clientWidth < 150) { + if (prButton && prButton.scrollWidth > prButton.clientWidth) { const labelSpan = prButton.querySelector('span:not(.codicon)') as HTMLElement | null; if (labelSpan) { labelSpan.textContent = localize('pullRequest.short', "PR"); From 14ccccdca617870be87820db074352213f8aae12 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 16:24:17 -0800 Subject: [PATCH 38/50] Remove redundant PR button label rendering logic and clean up CSS overflow property for actions container --- .../contrib/changesView/browser/changesView.ts | 12 ------------ .../changesView/browser/media/changesView.css | 1 - 2 files changed, 13 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 563486efd703a..be2f0b5b24186 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -558,7 +558,6 @@ export class ChangesViewPane extends ViewPane { const sessionResource = activeSessionResource.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; - // Always render with full label first reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, this.actionsContainer!, @@ -586,17 +585,6 @@ export class ChangesViewPane extends ViewPane { } } )); - - // After layout, shorten the PR button label if its text overflows - requestAnimationFrame(() => { - const prButton = this.actionsContainer?.querySelector('.flex-grow') as HTMLElement | null; - if (prButton && prButton.scrollWidth > prButton.clientWidth) { - const labelSpan = prButton.querySelector('span:not(.codicon)') as HTMLElement | null; - if (labelSpan) { - labelSpan.textContent = localize('pullRequest.short', "PR"); - } - } - }); })); } diff --git a/src/vs/sessions/contrib/changesView/browser/media/changesView.css b/src/vs/sessions/contrib/changesView/browser/media/changesView.css index 21b6d7dc52661..1300b886cbcd8 100644 --- a/src/vs/sessions/contrib/changesView/browser/media/changesView.css +++ b/src/vs/sessions/contrib/changesView/browser/media/changesView.css @@ -98,7 +98,6 @@ .changes-view-body .chat-editing-session-actions.outside-card { margin-bottom: 8px; justify-content: flex-end; - overflow: hidden; } /* Larger action buttons matching SCM ActionButton style */ From 756602da18d2dab05011fc9185f2d13714bc5753 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 16:26:55 -0800 Subject: [PATCH 39/50] Set GitHub context for open pull requests based on session resolution --- extensions/github/src/commands.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index f6af73536ef70..33acf5a406b87 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -93,6 +93,7 @@ function resolveSessionRepo(gitAPI: GitAPI, sessionMetadata: { worktreePath?: st async function checkOpenPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise { const resolved = resolveSessionRepo(gitAPI, sessionMetadata, false); if (!resolved) { + vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false); return; } @@ -107,7 +108,7 @@ async function checkOpenPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', openPRs.length > 0); } catch { - // Silently fail — leave context key unchanged + vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false); } } From f59869f7c619217a1aef0039442d66849c56891c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:29:06 +0000 Subject: [PATCH 40/50] Bump actions/checkout from 4 to 6 (#297973) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> --- .github/workflows/screenshot-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index 45136b1f8d207..0b082e81a24d9 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: lfs: true From cfa83636453f120250c87210fcd1f1be1efa9f2d Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 27 Feb 2026 12:00:28 +1100 Subject: [PATCH 41/50] fix(chat): ensure modes array is checked for length before inclusion check (#298129) --- .../chat/browser/widget/input/editor/chatInputCompletions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index 3cd4ffadcb57b..a03b216363ceb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -132,7 +132,7 @@ class SlashCommandCompletions extends Disposable { if (!widget.lockedAgentId) { return true; } - if (c.modes && !c.modes.includes(ChatModeKind.Agent)) { + if (c.modes && c.modes.length && !c.modes.includes(ChatModeKind.Agent)) { return false; } if (c.target && customAgentTarget && c.target !== customAgentTarget) { From 3ce018964083ce98d2f17e88cb10978f95b8a0ab Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:08:19 -0800 Subject: [PATCH 42/50] Sessions: customization improvements (#298122) * Refactor: unified IStorageSourceFilter replaces per-field filtering APIs Replace visibleStorageSources, getVisibleStorageSources(type), and excludedUserFileRoots with a single getStorageSourceFilter(type) returning IStorageSourceFilter with sources and includedUserFileRoots. - New IStorageSourceFilter interface with allowlist-based user root filtering - Shared applyStorageSourceFilter helper for list widget and counts - Sessions: hooks=workspace-only, prompts=all roots, others=CLI roots - AgenticPromptsService.getSourceFolders override for creation targeting - Remove chat.customizationsMenu.userStoragePath setting - Simplify resolveUserTargetDirectory to pure getSourceFolders delegate - Update all consumer call sites and tests * Fix sidebar/editor count mismatch and rename preferManualCreation Count functions now use the same data sources as loadItems(): - Agents: getCustomAgents() instead of listPromptFilesForStorage - Skills: findAgentSkills() - Prompts: getPromptSlashCommands() filtering out skills - Instructions: listPromptFiles() + listAgentInstructions() - Hooks: listPromptFiles() Rename preferManualCreation to isSessionsWindow for clarity. Add 50 tests for applyStorageSourceFilter and customizationCounts. * Add Developer: Customizations Debug command and fix hooks.json - Debug command opens untitled editor with full pipeline diagnostics - Rename 'Open Chat Customizations (Preview)' to 'Open Customizations (Preview)' - Fix hooks.json: add version field, use bash instead of command - Derive hook events from COPILOT_CLI_HOOK_TYPE_MAP schema automatically * Update AI_CUSTOMIZATIONS.md spec - Document IStorageSourceFilter, AgenticPromptsService, count consistency - Add debug panel section and updated file structure - Reflect isSessionsWindow rename * Remove verbose debug logs from list widget The Developer: Customizations Debug command provides better diagnostics. Remove noisy info-level logs that dump every item URI on every load. * Code review fixes: cache copilotRoot, remove dead getter, fix JSDoc * Add AI customizations manual test plan with 5 scenarios --- src/vs/sessions/AI_CUSTOMIZATIONS.md | 69 +- .../aiCustomizationWorkspaceService.ts | 56 +- .../contrib/chat/browser/promptsService.ts | 45 + .../browser/configuration.contribution.ts | 2 - .../sessions/browser/customizationCounts.ts | 132 +-- .../customizationsToolbar.contribution.ts | 22 +- .../sessions/browser/sessionsViewPane.ts | 2 +- .../test/browser/customizationCounts.test.ts | 796 ++++++++++++++++++ .../sessions/test/ai-customizations.test.md | 166 ++++ .../aiCustomizationDebugPanel.ts | 202 +++++ .../aiCustomizationListWidget.ts | 51 +- .../aiCustomizationManagement.contribution.ts | 26 +- .../aiCustomizationManagement.ts | 1 + .../aiCustomizationManagementEditor.ts | 36 +- .../aiCustomizationWorkspaceService.ts | 19 +- .../customizationCreatorService.ts | 35 +- .../contrib/chat/browser/chat.contribution.ts | 6 - .../common/aiCustomizationWorkspaceService.ts | 57 +- .../contrib/chat/common/constants.ts | 1 - .../applyStorageSourceFilter.test.ts | 237 ++++++ .../customizationCreatorService.test.ts | 103 +-- 21 files changed, 1761 insertions(+), 303 deletions(-) create mode 100644 src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts create mode 100644 src/vs/sessions/test/ai-customizations.test.md create mode 100644 src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/aiCustomization/applyStorageSourceFilter.test.ts diff --git a/src/vs/sessions/AI_CUSTOMIZATIONS.md b/src/vs/sessions/AI_CUSTOMIZATIONS.md index eb342a816ad34..a3309067c7f49 100644 --- a/src/vs/sessions/AI_CUSTOMIZATIONS.md +++ b/src/vs/sessions/AI_CUSTOMIZATIONS.md @@ -15,6 +15,7 @@ src/vs/workbench/contrib/chat/browser/aiCustomization/ ├── aiCustomizationManagementEditor.ts # SplitView list/editor ├── aiCustomizationManagementEditorInput.ts # Singleton input ├── aiCustomizationListWidget.ts # Search + grouped list +├── aiCustomizationDebugPanel.ts # Debug diagnostics panel ├── aiCustomizationWorkspaceService.ts # Core VS Code workspace service impl ├── customizationCreatorService.ts # AI-guided creation flow ├── mcpListWidget.ts # MCP servers section @@ -23,7 +24,7 @@ src/vs/workbench/contrib/chat/browser/aiCustomization/ └── aiCustomizationManagement.css src/vs/workbench/contrib/chat/common/ -└── aiCustomizationWorkspaceService.ts # IAICustomizationWorkspaceService interface +└── aiCustomizationWorkspaceService.ts # IAICustomizationWorkspaceService + IStorageSourceFilter ``` The tree view and overview live in `vs/sessions` (sessions window only): @@ -42,9 +43,10 @@ Sessions-specific overrides: ``` src/vs/sessions/contrib/chat/browser/ -└── aiCustomizationWorkspaceService.ts # Sessions workspace service override +├── aiCustomizationWorkspaceService.ts # Sessions workspace service override +└── promptsService.ts # AgenticPromptsService (CLI user roots) src/vs/sessions/contrib/sessions/browser/ -├── customizationCounts.ts # Source count utilities +├── customizationCounts.ts # Source count utilities (type-aware) └── customizationsToolbar.contribution.ts # Sidebar customization links ``` @@ -52,13 +54,66 @@ src/vs/sessions/contrib/sessions/browser/ The `IAICustomizationWorkspaceService` interface controls per-window behavior: -| Property | Core VS Code | Sessions Window | +| Property / Method | Core VS Code | Sessions Window | |----------|-------------|----------| -| `managementSections` | All sections except Models | Same | -| `visibleStorageSources` | workspace, user, extension, plugin | workspace, user only | -| `preferManualCreation` | `false` (AI generation primary) | `true` (file creation primary) | +| `managementSections` | All sections except Models | Same minus MCP | +| `getStorageSourceFilter(type)` | All sources, no user root filter | Per-type (see below) | +| `isSessionsWindow` | `false` | `true` | | `activeProjectRoot` | First workspace folder | Active session worktree | +### IStorageSourceFilter + +A unified per-type filter controlling which storage sources and user file roots are visible. +Replaces the old `visibleStorageSources`, `getVisibleStorageSources(type)`, and `excludedUserFileRoots`. + +```typescript +interface IStorageSourceFilter { + sources: readonly PromptsStorage[]; // Which storage groups to display + includedUserFileRoots?: readonly URI[]; // Allowlist for user roots (undefined = all) +} +``` + +The shared `applyStorageSourceFilter()` helper applies this filter to any `{uri, storage}` array. + +**Sessions filter behavior by type:** + +| Type | sources | includedUserFileRoots | +|------|---------|----------------------| +| Hooks | `[local]` | N/A | +| Prompts | `[local, user]` | `undefined` (all roots) | +| Agents, Skills, Instructions | `[local, user]` | `[~/.copilot, ~/.claude, ~/.agents]` | + +**Core VS Code:** All types use `[local, user, extension, plugin]` with no user root filter. + +### AgenticPromptsService (Sessions) + +Sessions overrides `PromptsService` via `AgenticPromptsService` (in `promptsService.ts`): + +- **Discovery**: `AgenticPromptFilesLocator` scopes workspace folders to the active session's worktree +- **Creation targets**: `getSourceFolders()` override replaces VS Code profile user roots with `~/.copilot/{subfolder}` for CLI compatibility +- **Hook folders**: Falls back to `.github/hooks` in the active worktree + +### Count Consistency + +`customizationCounts.ts` uses the **same data sources** as the list widget's `loadItems()`: + +| Type | Data Source | Notes | +|------|-------------|-------| +| Agents | `getCustomAgents()` | Parsed agents, not raw files | +| Skills | `findAgentSkills()` | Parsed skills with frontmatter | +| Prompts | `getPromptSlashCommands()` | Filters out skill-type commands | +| Instructions | `listPromptFiles()` + `listAgentInstructions()` | Includes AGENTS.md, CLAUDE.md etc. | +| Hooks | `listPromptFiles()` | Raw hook files | + +### Debug Panel + +Toggle via Command Palette: "Toggle Customizations Debug Panel". Shows a 4-stage pipeline view: + +1. **Raw PromptsService data** — per-storage file lists + type-specific extras +2. **After applyStorageSourceFilter** — what was removed and why +3. **Widget state** — allItems vs displayEntries with group counts +4. **Source/resolved folders** — creation targets and discovery order + ## Key Services - **Prompt discovery**: `IPromptsService` — parsing, lifecycle, storage enumeration diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 2bb4fe99b64e6..79dc93837c9e5 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { derived, IObservable } from '../../../../base/common/observable.js'; +import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; -import { IAICustomizationWorkspaceService, AICustomizationManagementSection } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; +import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; /** * Agent Sessions override of IAICustomizationWorkspaceService. @@ -23,14 +24,32 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization readonly activeProjectRoot: IObservable; - readonly excludedUserFileRoots: readonly URI[]; + /** + * CLI-accessible user directories for customization file filtering and creation. + */ + private readonly _cliUserRoots: readonly URI[]; + + /** + * Pre-built filter for types that should only show CLI-accessible user roots. + */ + private readonly _cliUserFilter: IStorageSourceFilter; constructor( @ISessionsManagementService private readonly sessionsService: ISessionsManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IPathService pathService: IPathService, ) { - this.excludedUserFileRoots = [userDataProfilesService.defaultProfile.promptsHome]; + const userHome = pathService.userHome({ preferLocal: true }); + this._cliUserRoots = [ + joinPath(userHome, '.copilot'), + joinPath(userHome, '.claude'), + joinPath(userHome, '.agents'), + ]; + this._cliUserFilter = { + sources: [PromptsStorage.local, PromptsStorage.user], + includedUserFileRoots: this._cliUserRoots, + }; + this.activeProjectRoot = derived(reader => { const session = this.sessionsService.activeSession.read(reader); return session?.worktree ?? session?.repository; @@ -52,19 +71,30 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization // AICustomizationManagementSection.McpServers, ]; - readonly visibleStorageSources: readonly PromptsStorage[] = [ - PromptsStorage.local, - PromptsStorage.user, - ]; + private static readonly _hooksFilter: IStorageSourceFilter = { + sources: [PromptsStorage.local], + }; - getVisibleStorageSources(type: PromptsType): readonly PromptsStorage[] { + private static readonly _allUserRootsFilter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user], + }; + + getStorageSourceFilter(type: PromptsType): IStorageSourceFilter { if (type === PromptsType.hook) { - return [PromptsStorage.local]; + return SessionsAICustomizationWorkspaceService._hooksFilter; + } + if (type === PromptsType.prompt) { + // Prompts are shown from all user roots (including VS Code profile) + return SessionsAICustomizationWorkspaceService._allUserRootsFilter; } - return this.visibleStorageSources; + // Other types only show user files from CLI-accessible roots (~/.copilot, ~/.claude, ~/.agents) + return this._cliUserFilter; } - readonly preferManualCreation = true; + /** + * Returns the CLI-accessible user directories (~/.copilot, ~/.claude, ~/.agents). + */ + readonly isSessionsWindow = true; async commitFiles(projectRoot: URI, fileUris: URI[]): Promise { const session = this.sessionsService.getActiveSession(); diff --git a/src/vs/sessions/contrib/chat/browser/promptsService.ts b/src/vs/sessions/contrib/chat/browser/promptsService.ts index 69c0e8e2497dd..96596f2de0c3f 100644 --- a/src/vs/sessions/contrib/chat/browser/promptsService.ts +++ b/src/vs/sessions/contrib/chat/browser/promptsService.ts @@ -13,6 +13,8 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { HOOKS_SOURCE_FOLDER } from '../../../../workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.js'; +import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; +import { IPromptPath, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { IWorkbenchEnvironmentService } from '../../../../workbench/services/environment/common/environmentService.js'; import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { ISearchService } from '../../../../workbench/services/search/common/search.js'; @@ -20,9 +22,38 @@ import { IUserDataProfileService } from '../../../../workbench/services/userData import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; export class AgenticPromptsService extends PromptsService { + private _copilotRoot: URI | undefined; + protected override createPromptFilesLocator(): PromptFilesLocator { return this.instantiationService.createInstance(AgenticPromptFilesLocator); } + + private getCopilotRoot(): URI { + if (!this._copilotRoot) { + const pathService = this.instantiationService.invokeFunction(accessor => accessor.get(IPathService)); + this._copilotRoot = joinPath(pathService.userHome({ preferLocal: true }), '.copilot'); + } + return this._copilotRoot; + } + + /** + * Override to use ~/.copilot as the user-level source folder for creation, + * instead of the VS Code profile's promptsHome. + */ + public override async getSourceFolders(type: PromptsType): Promise { + const folders = await super.getSourceFolders(type); + const copilotRoot = this.getCopilotRoot(); + // Replace any user-storage folders with the CLI-accessible ~/.copilot root + return folders.map(folder => { + if (folder.storage === PromptsStorage.user) { + const subfolder = getCliUserSubfolder(type); + return subfolder + ? { ...folder, uri: joinPath(copilotRoot, subfolder) } + : folder; + } + return folder; + }); + } } class AgenticPromptFilesLocator extends PromptFilesLocator { @@ -91,3 +122,17 @@ class AgenticPromptFilesLocator extends PromptFilesLocator { } } +/** + * Returns the subfolder name under ~/.copilot/ for a given customization type. + * Used to determine the CLI-accessible user creation target. + */ +function getCliUserSubfolder(type: PromptsType): string | undefined { + switch (type) { + case PromptsType.instructions: return 'instructions'; + case PromptsType.skill: return 'skills'; + case PromptsType.agent: return 'agents'; + case PromptsType.prompt: return 'prompts'; + default: return undefined; + } +} + diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index b1730dd1897e3..718a5ad73e28a 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -30,8 +30,6 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'github.copilot.chat.languageContext.typescript.enabled': true, 'github.copilot.chat.cli.mcp.enabled': true, - 'chat.customizationsMenu.userStoragePath': '~/.copilot', - 'inlineChat.affordance': 'editor', 'inlineChat.renderMode': 'hover', diff --git a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts index 98015af7a0ab0..fac485cf511c6 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { URI } from '../../../../base/common/uri.js'; import { isEqualOrParent } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; - -import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; export interface ISourceCounts { readonly workspace: number; @@ -24,9 +24,9 @@ const storageToCountKey: Partial> = [PromptsStorage.extension]: 'extension', }; -export function getSourceCountsTotal(counts: ISourceCounts, workspaceService: IAICustomizationWorkspaceService, type: PromptsType): number { +export function getSourceCountsTotal(counts: ISourceCounts, filter: IStorageSourceFilter): number { let total = 0; - for (const storage of workspaceService.getVisibleStorageSources(type)) { + for (const storage of filter.sources) { const key = storageToCountKey[storage]; if (key) { total += counts[key]; @@ -36,58 +36,86 @@ export function getSourceCountsTotal(counts: ISourceCounts, workspaceService: IA } /** - * Returns true if the URI should be excluded based on excluded user file roots. + * Gets source counts for a prompt type, using the SAME data sources as + * loadItems() in the list widget to avoid count mismatches. */ -function isExcludedUserFile(uri: URI, excludedRoots: readonly URI[]): boolean { - return excludedRoots.some(root => isEqualOrParent(uri, root)); -} +export async function getSourceCounts( + promptsService: IPromptsService, + promptType: PromptsType, + filter: IStorageSourceFilter, + workspaceContextService: IWorkspaceContextService, + workspaceService: IAICustomizationWorkspaceService, +): Promise { + const items: { storage: PromptsStorage; uri: URI }[] = []; -export async function getPromptSourceCounts(promptsService: IPromptsService, promptType: PromptsType, excludedUserFileRoots: readonly URI[] = []): Promise { - const [workspaceItems, userItems, extensionItems] = await Promise.all([ - promptsService.listPromptFilesForStorage(promptType, PromptsStorage.local, CancellationToken.None), - promptsService.listPromptFilesForStorage(promptType, PromptsStorage.user, CancellationToken.None), - promptsService.listPromptFilesForStorage(promptType, PromptsStorage.extension, CancellationToken.None), - ]); - const filteredUserItems = excludedUserFileRoots.length > 0 - ? userItems.filter(item => !isExcludedUserFile(item.uri, excludedUserFileRoots)) - : userItems; - return { - workspace: workspaceItems.length, - user: filteredUserItems.length, - extension: extensionItems.length, - }; -} - -export async function getSkillSourceCounts(promptsService: IPromptsService, excludedUserFileRoots: readonly URI[] = []): Promise { - const skills = await promptsService.findAgentSkills(CancellationToken.None); - if (!skills || skills.length === 0) { - return { workspace: 0, user: 0, extension: 0 }; + if (promptType === PromptsType.agent) { + // Must match loadItems: uses getCustomAgents() + const agents = await promptsService.getCustomAgents(CancellationToken.None); + for (const a of agents) { + items.push({ storage: a.source.storage, uri: a.uri }); + } + } else if (promptType === PromptsType.skill) { + // Must match loadItems: uses findAgentSkills() + const skills = await promptsService.findAgentSkills(CancellationToken.None); + for (const s of skills ?? []) { + items.push({ storage: s.storage, uri: s.uri }); + } + } else if (promptType === PromptsType.prompt) { + // Must match loadItems: uses getPromptSlashCommands() filtering out skills + const commands = await promptsService.getPromptSlashCommands(CancellationToken.None); + for (const c of commands) { + if (c.promptPath.type === PromptsType.skill) { + continue; + } + items.push({ storage: c.promptPath.storage, uri: c.promptPath.uri }); + } + } else if (promptType === PromptsType.instructions) { + // Must match loadItems: uses listPromptFiles + listAgentInstructions + const promptFiles = await promptsService.listPromptFiles(promptType, CancellationToken.None); + for (const f of promptFiles) { + items.push({ storage: f.storage, uri: f.uri }); + } + const agentInstructions = await promptsService.listAgentInstructions(CancellationToken.None, undefined); + const workspaceFolderUris = workspaceContextService.getWorkspace().folders.map(f => f.uri); + const activeRoot = workspaceService.getActiveProjectRoot(); + if (activeRoot) { + workspaceFolderUris.push(activeRoot); + } + for (const file of agentInstructions) { + const isWorkspaceFile = workspaceFolderUris.some(root => isEqualOrParent(file.uri, root)); + items.push({ + storage: isWorkspaceFile ? PromptsStorage.local : PromptsStorage.user, + uri: file.uri, + }); + } + } else { + // hooks and anything else: uses listPromptFiles + const files = await promptsService.listPromptFiles(promptType, CancellationToken.None); + for (const f of files) { + items.push({ storage: f.storage, uri: f.uri }); + } } - const userSkills = skills.filter(s => s.storage === PromptsStorage.user); - const filteredUserSkills = excludedUserFileRoots.length > 0 - ? userSkills.filter(s => !isExcludedUserFile(s.uri, excludedUserFileRoots)) - : userSkills; + + // Apply the same storage source filter as the list widget + const filtered = applyStorageSourceFilter(items, filter); return { - workspace: skills.filter(s => s.storage === PromptsStorage.local).length, - user: filteredUserSkills.length, - extension: skills.filter(s => s.storage === PromptsStorage.extension).length, + workspace: filtered.filter(i => i.storage === PromptsStorage.local).length, + user: filtered.filter(i => i.storage === PromptsStorage.user).length, + extension: filtered.filter(i => i.storage === PromptsStorage.extension).length, }; } -export async function getCustomizationTotalCount(promptsService: IPromptsService, mcpService: IMcpService, workspaceService: IAICustomizationWorkspaceService): Promise { - const excluded = workspaceService.excludedUserFileRoots; - const [agentCounts, skillCounts, instructionCounts, promptCounts, hookCounts] = await Promise.all([ - getPromptSourceCounts(promptsService, PromptsType.agent, excluded), - getSkillSourceCounts(promptsService, excluded), - getPromptSourceCounts(promptsService, PromptsType.instructions, excluded), - getPromptSourceCounts(promptsService, PromptsType.prompt, excluded), - getPromptSourceCounts(promptsService, PromptsType.hook, excluded), - ]); - - return getSourceCountsTotal(agentCounts, workspaceService, PromptsType.agent) - + getSourceCountsTotal(skillCounts, workspaceService, PromptsType.skill) - + getSourceCountsTotal(instructionCounts, workspaceService, PromptsType.instructions) - + getSourceCountsTotal(promptCounts, workspaceService, PromptsType.prompt) - + getSourceCountsTotal(hookCounts, workspaceService, PromptsType.hook) - + mcpService.servers.get().length; +export async function getCustomizationTotalCount( + promptsService: IPromptsService, + mcpService: IMcpService, + workspaceService: IAICustomizationWorkspaceService, + workspaceContextService: IWorkspaceContextService, +): Promise { + const types: PromptsType[] = [PromptsType.agent, PromptsType.skill, PromptsType.instructions, PromptsType.prompt, PromptsType.hook]; + const results = await Promise.all(types.map(type => { + const filter = workspaceService.getStorageSourceFilter(type); + return getSourceCounts(promptsService, type, filter, workspaceContextService, workspaceService) + .then(counts => getSourceCountsTotal(counts, filter)); + })); + return results.reduce((sum, n) => sum + n, 0) + mcpService.servers.get().length; } diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index 827d162f79470..c5c913e179726 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -29,19 +29,16 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { ISessionsManagementService } from './sessionsManagementService.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { getPromptSourceCounts, getSkillSourceCounts, getSourceCountsTotal, ISourceCounts } from './customizationCounts.js'; +import { getSourceCounts, getSourceCountsTotal, ISourceCounts } from './customizationCounts.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; -import { URI } from '../../../../base/common/uri.js'; - interface ICustomizationItemConfig { readonly id: string; readonly label: string; readonly icon: ThemeIcon; readonly section: AICustomizationManagementSection; readonly promptType?: PromptsType; - readonly getSourceCounts?: (promptsService: IPromptsService, excludedUserFileRoots: readonly URI[]) => Promise; readonly getCount?: (languageModelsService: ILanguageModelsService, mcpService: IMcpService) => Promise; } @@ -52,7 +49,6 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ icon: agentIcon, section: AICustomizationManagementSection.Agents, promptType: PromptsType.agent, - getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.agent, ex), }, { id: 'sessions.customization.skills', @@ -60,7 +56,6 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ icon: skillIcon, section: AICustomizationManagementSection.Skills, promptType: PromptsType.skill, - getSourceCounts: (ps, ex) => getSkillSourceCounts(ps, ex), }, { id: 'sessions.customization.instructions', @@ -68,7 +63,6 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ icon: instructionsIcon, section: AICustomizationManagementSection.Instructions, promptType: PromptsType.instructions, - getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.instructions, ex), }, { id: 'sessions.customization.prompts', @@ -76,7 +70,6 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ icon: promptIcon, section: AICustomizationManagementSection.Prompts, promptType: PromptsType.prompt, - getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.prompt, ex), }, { id: 'sessions.customization.hooks', @@ -84,7 +77,6 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ icon: hookIcon, section: AICustomizationManagementSection.Hooks, promptType: PromptsType.hook, - getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.hook, ex), }, // TODO: Re-enable MCP Servers once CLI MCP configuration is unified with VS Code ]; @@ -167,8 +159,10 @@ class CustomizationLinkViewItem extends ActionViewItem { return; } - if (this._config.getSourceCounts) { - const counts = await this._config.getSourceCounts(this._promptsService, this._workspaceService.excludedUserFileRoots); + if (this._config.promptType) { + const type = this._config.promptType; + const filter = this._workspaceService.getStorageSourceFilter(type); + const counts = await getSourceCounts(this._promptsService, type, filter, this._workspaceContextService, this._workspaceService); this._renderSourceCounts(this._countContainer, counts); } else if (this._config.getCount) { const count = await this._config.getCount(this._languageModelsService, this._mcpService); @@ -179,14 +173,14 @@ class CustomizationLinkViewItem extends ActionViewItem { private _renderSourceCounts(container: HTMLElement, counts: ISourceCounts): void { container.textContent = ''; const type = this._config.promptType; - const visibleSources = type ? this._workspaceService.getVisibleStorageSources(type) : this._workspaceService.visibleStorageSources; - const total = getSourceCountsTotal(counts, this._workspaceService, type ?? PromptsType.prompt); + const filter = type ? this._workspaceService.getStorageSourceFilter(type) : this._workspaceService.getStorageSourceFilter(PromptsType.prompt); + const total = getSourceCountsTotal(counts, filter); container.classList.toggle('hidden', total === 0); if (total === 0) { return; } - const visibleSourcesSet = new Set(visibleSources); + const visibleSourcesSet = new Set(filter.sources); const sources: { storage: PromptsStorage; count: number; icon: ThemeIcon; title: string }[] = [ { storage: PromptsStorage.local, count: counts.workspace, icon: workspaceIcon, title: localize('workspaceCount', "{0} from workspace", counts.workspace) }, { storage: PromptsStorage.user, count: counts.user, icon: userIcon, title: localize('userCount', "{0} from user", counts.user) }, diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index b8ec6ff4e1dad..d53188c77bd1a 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -233,7 +233,7 @@ export class AgenticSessionsViewPane extends ViewPane { let updateCountRequestId = 0; const updateHeaderTotalCount = async () => { const requestId = ++updateCountRequestId; - const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService); + const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService); if (requestId !== updateCountRequestId) { return; } diff --git a/src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts b/src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts new file mode 100644 index 0000000000000..9a3ce3ee94220 --- /dev/null +++ b/src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts @@ -0,0 +1,796 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { PromptsType } from '../../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; +import { IPromptsService, PromptsStorage, IPromptPath, ILocalPromptPath, IUserPromptPath, IExtensionPromptPath, IResolvedAgentFile, AgentFileType } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IWorkspaceContextService, IWorkspace, IWorkspaceFolder, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; +import { getSourceCounts, getSourceCountsTotal, getCustomizationTotalCount } from '../../browser/customizationCounts.js'; +import { IMcpService } from '../../../../../workbench/contrib/mcp/common/mcpTypes.js'; +import { Event } from '../../../../../base/common/event.js'; +import { observableValue } from '../../../../../base/common/observable.js'; + +function localFile(path: string): ILocalPromptPath { + return { uri: URI.file(path), storage: PromptsStorage.local, type: PromptsType.instructions }; +} + +function userFile(path: string): IUserPromptPath { + return { uri: URI.file(path), storage: PromptsStorage.user, type: PromptsType.instructions }; +} + +function extensionFile(path: string): IExtensionPromptPath { + return { + uri: URI.file(path), + storage: PromptsStorage.extension, + type: PromptsType.instructions, + extension: undefined!, + source: undefined!, + }; +} + +function agentInstructionFile(path: string): IResolvedAgentFile { + return { uri: URI.file(path), realPath: undefined, type: AgentFileType.agentsMd }; +} + +function makeWorkspaceFolder(path: string, name?: string): IWorkspaceFolder { + const uri = URI.file(path); + return { + uri, + name: name ?? path.split('/').pop()!, + index: 0, + toResource: (rel: string) => URI.joinPath(uri, rel), + }; +} + +function createMockPromptsService(opts: { + localFiles?: IPromptPath[]; + userFiles?: IPromptPath[]; + extensionFiles?: IPromptPath[]; + allFiles?: IPromptPath[]; + agentInstructions?: IResolvedAgentFile[]; + agents?: { name: string; uri: URI; storage: PromptsStorage }[]; + skills?: { name: string; uri: URI; storage: PromptsStorage }[]; + commands?: { name: string; uri: URI; storage: PromptsStorage; type: PromptsType }[]; +} = {}): IPromptsService { + return { + listPromptFilesForStorage: async (type: PromptsType, storage: PromptsStorage) => { + if (storage === PromptsStorage.local) { return opts.localFiles ?? []; } + if (storage === PromptsStorage.user) { return opts.userFiles ?? []; } + if (storage === PromptsStorage.extension) { return opts.extensionFiles ?? []; } + return []; + }, + listPromptFiles: async () => opts.allFiles ?? [...(opts.localFiles ?? []), ...(opts.userFiles ?? []), ...(opts.extensionFiles ?? [])], + listAgentInstructions: async () => opts.agentInstructions ?? [], + getCustomAgents: async () => (opts.agents ?? []).map(a => ({ + name: a.name, + uri: a.uri, + source: { storage: a.storage }, + })), + findAgentSkills: async () => (opts.skills ?? []).map(s => ({ + name: s.name, + uri: s.uri, + storage: s.storage, + })), + getPromptSlashCommands: async () => (opts.commands ?? []).map(c => ({ + name: c.name, + promptPath: { uri: c.uri, storage: c.storage, type: c.type }, + })), + getSourceFolders: async () => [], + getResolvedSourceFolders: async () => [], + onDidChangeCustomAgents: Event.None, + onDidChangeSlashCommands: Event.None, + } as unknown as IPromptsService; +} + +function createMockWorkspaceService(opts: { + activeRoot?: URI; + filter?: IStorageSourceFilter; +} = {}): IAICustomizationWorkspaceService { + const defaultFilter: IStorageSourceFilter = opts.filter ?? { + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension], + }; + return { + _serviceBrand: undefined, + activeProjectRoot: observableValue('test', opts.activeRoot), + getActiveProjectRoot: () => opts.activeRoot, + managementSections: [], + getStorageSourceFilter: () => defaultFilter, + preferManualCreation: false, + commitFiles: async () => { }, + generateCustomization: async () => { }, + } as unknown as IAICustomizationWorkspaceService; +} + +function createMockWorkspaceContextService(folders: IWorkspaceFolder[]): IWorkspaceContextService { + return { + getWorkspace: () => ({ folders } as IWorkspace), + getWorkbenchState: () => WorkbenchState.FOLDER, + getWorkspaceFolder: () => folders[0], + onDidChangeWorkspaceFolders: Event.None, + onDidChangeWorkbenchState: Event.None, + onDidChangeWorkspaceName: Event.None, + isInsideWorkspace: () => true, + } as unknown as IWorkspaceContextService; +} + +suite('customizationCounts', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const workspaceRoot = URI.file('/workspace'); + const workspaceFolder = makeWorkspaceFolder('/workspace'); + + suite('getSourceCountsTotal', () => { + test('sums only visible sources', () => { + const counts = { workspace: 5, user: 3, extension: 2 }; + const filter: IStorageSourceFilter = { sources: [PromptsStorage.local, PromptsStorage.user] }; + assert.strictEqual(getSourceCountsTotal(counts, filter), 8); + }); + + test('returns 0 for empty sources', () => { + const counts = { workspace: 5, user: 3, extension: 2 }; + const filter: IStorageSourceFilter = { sources: [] }; + assert.strictEqual(getSourceCountsTotal(counts, filter), 0); + }); + + test('sums all sources', () => { + const counts = { workspace: 5, user: 3, extension: 2 }; + const filter: IStorageSourceFilter = { sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension] }; + assert.strictEqual(getSourceCountsTotal(counts, filter), 10); + }); + + test('handles single source', () => { + const counts = { workspace: 7, user: 0, extension: 0 }; + const filter: IStorageSourceFilter = { sources: [PromptsStorage.local] }; + assert.strictEqual(getSourceCountsTotal(counts, filter), 7); + }); + + test('ignores plugin storage in totals (not in ISourceCounts)', () => { + const counts = { workspace: 1, user: 1, extension: 1 }; + const filter: IStorageSourceFilter = { sources: [PromptsStorage.plugin] }; + assert.strictEqual(getSourceCountsTotal(counts, filter), 0); + }); + }); + + suite('getSourceCounts - instructions', () => { + test('includes agent instruction files in workspace count', async () => { + const promptsService = createMockPromptsService({ + localFiles: [ + localFile('/workspace/.github/instructions/a.instructions.md'), + ], + userFiles: [], + extensionFiles: [], + allFiles: [ + localFile('/workspace/.github/instructions/a.instructions.md'), + ], + agentInstructions: [ + agentInstructionFile('/workspace/AGENTS.md'), + agentInstructionFile('/workspace/.github/copilot-instructions.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.instructions, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, + workspaceService, + ); + + // 1 .instructions.md + 2 agent instruction files = 3 workspace + assert.strictEqual(counts.workspace, 3); + assert.strictEqual(counts.user, 0); + }); + + test('classifies agent instructions outside workspace as user', async () => { + const promptsService = createMockPromptsService({ + localFiles: [], + userFiles: [], + extensionFiles: [], + allFiles: [], + agentInstructions: [ + agentInstructionFile('/home/user/.claude/CLAUDE.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.instructions, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, + workspaceService, + ); + + assert.strictEqual(counts.workspace, 0); + assert.strictEqual(counts.user, 1); + }); + + test('agent instructions under active root classified as workspace', async () => { + // Active root might not be in getWorkspace().folders (e.g. sessions worktree), + // but should still count as workspace + const activeRoot = URI.file('/session/worktree'); + const promptsService = createMockPromptsService({ + allFiles: [], + agentInstructions: [ + agentInstructionFile('/session/worktree/AGENTS.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot }); + // No workspace folders match — but active root does + const contextService = createMockWorkspaceContextService([]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.instructions, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, + workspaceService, + ); + + assert.strictEqual(counts.workspace, 1); + assert.strictEqual(counts.user, 0); + }); + + test('no agent instructions returns only prompt file counts', async () => { + const promptsService = createMockPromptsService({ + allFiles: [ + localFile('/workspace/.github/instructions/a.instructions.md'), + localFile('/workspace/.github/instructions/b.instructions.md'), + ], + agentInstructions: [], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.instructions, + { sources: [PromptsStorage.local] }, + contextService, + workspaceService, + ); + + assert.strictEqual(counts.workspace, 2); + }); + + test('mixed agent instructions across workspace and user', async () => { + const promptsService = createMockPromptsService({ + allFiles: [ + localFile('/workspace/.github/instructions/rules.instructions.md'), + ], + agentInstructions: [ + agentInstructionFile('/workspace/AGENTS.md'), + agentInstructionFile('/workspace/CLAUDE.md'), + agentInstructionFile('/home/user/.claude/CLAUDE.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.instructions, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, + workspaceService, + ); + + // 1 .instructions.md + 2 workspace agent files = 3 + assert.strictEqual(counts.workspace, 3); + // 1 user-level CLAUDE.md + assert.strictEqual(counts.user, 1); + }); + }); + + suite('getSourceCounts - agents', () => { + test('uses getCustomAgents instead of listPromptFilesForStorage', async () => { + const promptsService = createMockPromptsService({ + // listPromptFilesForStorage would return these — but agents should use getCustomAgents + localFiles: [localFile('/workspace/.github/agents/a.agent.md')], + agents: [ + { name: 'agent-a', uri: URI.file('/workspace/.github/agents/a.agent.md'), storage: PromptsStorage.local }, + { name: 'agent-b', uri: URI.file('/workspace/.github/agents/b.agent.md'), storage: PromptsStorage.local }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.agent, + { sources: [PromptsStorage.local] }, + contextService, + workspaceService, + ); + + // Should use getCustomAgents (2), not listPromptFilesForStorage (1) + assert.strictEqual(counts.workspace, 2); + }); + + test('counts agents across storage types', async () => { + const promptsService = createMockPromptsService({ + agents: [ + { name: 'local-agent', uri: URI.file('/workspace/.github/agents/a.agent.md'), storage: PromptsStorage.local }, + { name: 'user-agent', uri: URI.file('/home/.claude/agents/b.agent.md'), storage: PromptsStorage.user }, + { name: 'ext-agent', uri: URI.file('/ext/agents/c.agent.md'), storage: PromptsStorage.extension }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.agent, + { sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension] }, + contextService, + workspaceService, + ); + + assert.deepStrictEqual(counts, { workspace: 1, user: 1, extension: 1 }); + }); + + test('empty agents returns all zeros', async () => { + const promptsService = createMockPromptsService({ agents: [] }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.agent, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, workspaceService, + ); + + assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0 }); + }); + }); + + suite('getSourceCounts - skills', () => { + test('uses findAgentSkills', async () => { + const promptsService = createMockPromptsService({ + skills: [ + { name: 'skill-a', uri: URI.file('/workspace/.github/skills/a/SKILL.md'), storage: PromptsStorage.local }, + { name: 'skill-b', uri: URI.file('/home/user/.copilot/skills/b/SKILL.md'), storage: PromptsStorage.user }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.skill, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, + workspaceService, + ); + + assert.strictEqual(counts.workspace, 1); + assert.strictEqual(counts.user, 1); + }); + + test('empty skills returns zeros', async () => { + const promptsService = createMockPromptsService({ skills: [] }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.skill, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, workspaceService, + ); + + assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0 }); + }); + + test('skills filtered by storage source filter', async () => { + const promptsService = createMockPromptsService({ + skills: [ + { name: 'skill-a', uri: URI.file('/workspace/.github/skills/a/SKILL.md'), storage: PromptsStorage.local }, + { name: 'skill-b', uri: URI.file('/home/user/.copilot/skills/b/SKILL.md'), storage: PromptsStorage.user }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + // Only local sources visible + const counts = await getSourceCounts( + promptsService, PromptsType.skill, + { sources: [PromptsStorage.local] }, + contextService, workspaceService, + ); + + assert.strictEqual(counts.workspace, 1); + assert.strictEqual(counts.user, 0); + }); + }); + + suite('getSourceCounts - prompts', () => { + test('uses getPromptSlashCommands and filters out skills', async () => { + const promptsService = createMockPromptsService({ + commands: [ + { name: 'my-prompt', uri: URI.file('/workspace/.github/prompts/a.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt }, + { name: 'my-skill', uri: URI.file('/workspace/.github/skills/b/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.prompt, + { sources: [PromptsStorage.local] }, + contextService, + workspaceService, + ); + + // Should exclude the skill command + assert.strictEqual(counts.workspace, 1); + }); + + test('counts prompts across storage types', async () => { + const promptsService = createMockPromptsService({ + commands: [ + { name: 'wp', uri: URI.file('/workspace/.github/prompts/a.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt }, + { name: 'up', uri: URI.file('/home/user/prompts/b.prompt.md'), storage: PromptsStorage.user, type: PromptsType.prompt }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.prompt, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, workspaceService, + ); + + assert.deepStrictEqual(counts, { workspace: 1, user: 1, extension: 0 }); + }); + + test('all skills are excluded from prompt counts', async () => { + const promptsService = createMockPromptsService({ + commands: [ + { name: 's1', uri: URI.file('/w/s1/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill }, + { name: 's2', uri: URI.file('/w/s2/SKILL.md'), storage: PromptsStorage.user, type: PromptsType.skill }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.prompt, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, workspaceService, + ); + + assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0 }); + }); + }); + + suite('getSourceCounts - hooks', () => { + test('uses listPromptFiles for hooks', async () => { + const promptsService = createMockPromptsService({ + allFiles: [ + localFile('/workspace/.github/hooks/pre-commit.json'), + localFile('/workspace/.claude/settings.json'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.hook, + { sources: [PromptsStorage.local] }, + contextService, workspaceService, + ); + + assert.strictEqual(counts.workspace, 2); + }); + + test('hooks with only local source excludes user hooks', async () => { + const promptsService = createMockPromptsService({ + allFiles: [ + localFile('/workspace/.github/hooks/pre-commit.json'), + userFile('/home/user/.claude/settings.json'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.hook, + { sources: [PromptsStorage.local] }, + contextService, workspaceService, + ); + + assert.strictEqual(counts.workspace, 1); + assert.strictEqual(counts.user, 0); + }); + }); + + suite('getSourceCounts - filter', () => { + test('applies includedUserFileRoots filter', async () => { + const copilotRoot = URI.file('/home/user/.copilot'); + const promptsService = createMockPromptsService({ + allFiles: [ + localFile('/workspace/.github/instructions/a.instructions.md'), + userFile('/home/user/.copilot/instructions/b.instructions.md'), + userFile('/home/user/.vscode/instructions/c.instructions.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.instructions, + { + sources: [PromptsStorage.local, PromptsStorage.user], + includedUserFileRoots: [copilotRoot], + }, + contextService, + workspaceService, + ); + + assert.strictEqual(counts.workspace, 1); + // Only the copilot file passes, not the vscode profile file + assert.strictEqual(counts.user, 1); + }); + + test('excludes storage types not in sources', async () => { + const promptsService = createMockPromptsService({ + allFiles: [ + localFile('/workspace/.github/instructions/a.instructions.md'), + extensionFile('/ext/instructions/b.instructions.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, + PromptsType.instructions, + { sources: [PromptsStorage.local] }, + contextService, + workspaceService, + ); + + assert.strictEqual(counts.workspace, 1); + assert.strictEqual(counts.extension, 0); + }); + + test('includedUserFileRoots with multiple roots', async () => { + const copilotRoot = URI.file('/home/user/.copilot'); + const claudeRoot = URI.file('/home/user/.claude'); + const promptsService = createMockPromptsService({ + allFiles: [ + userFile('/home/user/.copilot/instructions/a.instructions.md'), + userFile('/home/user/.claude/rules/b.md'), + userFile('/home/user/.vscode/instructions/c.instructions.md'), + userFile('/home/user/.agents/instructions/d.instructions.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.instructions, + { + sources: [PromptsStorage.local, PromptsStorage.user], + includedUserFileRoots: [copilotRoot, claudeRoot], + }, + contextService, workspaceService, + ); + + // copilot + claude pass, vscode + agents don't + assert.strictEqual(counts.user, 2); + }); + + test('undefined includedUserFileRoots shows all user files', async () => { + const promptsService = createMockPromptsService({ + allFiles: [ + userFile('/home/user/.copilot/instructions/a.instructions.md'), + userFile('/home/user/.vscode/instructions/b.instructions.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.instructions, + { sources: [PromptsStorage.user] }, + contextService, workspaceService, + ); + + assert.strictEqual(counts.user, 2); + }); + }); + + suite('getCustomizationTotalCount', () => { + test('sums all sections', async () => { + const promptsService = createMockPromptsService({ + agents: [ + { name: 'a', uri: URI.file('/w/a.agent.md'), storage: PromptsStorage.local }, + ], + skills: [ + { name: 's', uri: URI.file('/w/s/SKILL.md'), storage: PromptsStorage.local }, + ], + commands: [ + { name: 'p', uri: URI.file('/w/p.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt }, + ], + }); + const mcpService = { + servers: observableValue('test', [{ id: 'srv1' }]), + } as unknown as IMcpService; + const workspaceService = createMockWorkspaceService({ + activeRoot: URI.file('/w'), + filter: { sources: [PromptsStorage.local] }, + }); + const contextService = createMockWorkspaceContextService([makeWorkspaceFolder('/w')]); + + const total = await getCustomizationTotalCount(promptsService, mcpService, workspaceService, contextService); + + // 1 agent + 1 skill + 0 instructions + 1 prompt + 0 hooks + 1 mcp = 4 + assert.strictEqual(total, 4); + }); + + test('empty workspace returns only mcp count', async () => { + const promptsService = createMockPromptsService({}); + const mcpService = { + servers: observableValue('test', [{ id: 's1' }, { id: 's2' }]), + } as unknown as IMcpService; + const workspaceService = createMockWorkspaceService({ + filter: { sources: [PromptsStorage.local] }, + }); + const contextService = createMockWorkspaceContextService([]); + + const total = await getCustomizationTotalCount(promptsService, mcpService, workspaceService, contextService); + + assert.strictEqual(total, 2); // just 2 mcp servers + }); + + test('includes instructions with agent files in count', async () => { + const instructionFiles = [ + localFile('/w/.github/instructions/a.instructions.md'), + ]; + const promptsService = createMockPromptsService({ + allFiles: instructionFiles, + agentInstructions: [ + agentInstructionFile('/w/AGENTS.md'), + ], + }); + // Override listPromptFiles to only return files for instructions type + promptsService.listPromptFiles = async (type: PromptsType) => { + return type === PromptsType.instructions ? instructionFiles : []; + }; + const mcpService = { + servers: observableValue('test', []), + } as unknown as IMcpService; + const workspaceService = createMockWorkspaceService({ + activeRoot: URI.file('/w'), + filter: { sources: [PromptsStorage.local] }, + }); + const contextService = createMockWorkspaceContextService([makeWorkspaceFolder('/w')]); + + const total = await getCustomizationTotalCount(promptsService, mcpService, workspaceService, contextService); + + // 0 agents + 0 skills + 2 instructions (1 file + 1 AGENTS.md) + 0 prompts + 0 hooks + 0 mcp = 2 + assert.strictEqual(total, 2); + }); + }); + + suite('data source consistency', () => { + // These tests verify that getSourceCounts uses the same data sources + // as the list widget's loadItems() — the root cause of the count mismatch bug. + + test('instructions count matches widget: listPromptFiles + listAgentInstructions', async () => { + // Scenario: 13 .instructions.md files + 2 agent instruction files = 15 total + // The old bug: sidebar showed 13 (only listPromptFilesForStorage), + // editor showed 15 (listPromptFiles + listAgentInstructions) + const instructionFiles = Array.from({ length: 13 }, (_, i) => + localFile(`/workspace/.github/instructions/rule-${i}.instructions.md`) + ); + const promptsService = createMockPromptsService({ + localFiles: instructionFiles, + allFiles: instructionFiles, + agentInstructions: [ + agentInstructionFile('/workspace/AGENTS.md'), + agentInstructionFile('/workspace/.github/copilot-instructions.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.instructions, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, workspaceService, + ); + + // Must be 15, not 13 + assert.strictEqual(counts.workspace, 15); + }); + + test('agents count uses getCustomAgents not listPromptFilesForStorage', async () => { + // getCustomAgents parses frontmatter and may exclude invalid files + const promptsService = createMockPromptsService({ + // Raw file count would be 3 + localFiles: [ + localFile('/workspace/.github/agents/a.agent.md'), + localFile('/workspace/.github/agents/b.agent.md'), + localFile('/workspace/.github/agents/README.md'), // would be excluded by getCustomAgents + ], + // But parsed custom agents is only 2 + agents: [ + { name: 'agent-a', uri: URI.file('/workspace/.github/agents/a.agent.md'), storage: PromptsStorage.local }, + { name: 'agent-b', uri: URI.file('/workspace/.github/agents/b.agent.md'), storage: PromptsStorage.local }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.agent, + { sources: [PromptsStorage.local] }, + contextService, workspaceService, + ); + + // Must use getCustomAgents count (2), not raw file count (3) + assert.strictEqual(counts.workspace, 2); + }); + + test('prompts count excludes skills to match widget', async () => { + // The widget's loadItems filters out skill-type commands. + // Count must do the same. + const promptsService = createMockPromptsService({ + localFiles: [ + localFile('/workspace/.github/prompts/a.prompt.md'), + localFile('/workspace/.github/prompts/b.prompt.md'), + ], + commands: [ + { name: 'prompt-a', uri: URI.file('/workspace/.github/prompts/a.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt }, + { name: 'prompt-b', uri: URI.file('/workspace/.github/prompts/b.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt }, + { name: 'skill-x', uri: URI.file('/workspace/.github/skills/x/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill }, + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: workspaceRoot }); + const contextService = createMockWorkspaceContextService([workspaceFolder]); + + const counts = await getSourceCounts( + promptsService, PromptsType.prompt, + { sources: [PromptsStorage.local] }, + contextService, workspaceService, + ); + + // Must be 2 (prompts only), not 3 (including skill) + assert.strictEqual(counts.workspace, 2); + }); + + test('no active root: agent instructions classified as user', async () => { + const promptsService = createMockPromptsService({ + allFiles: [], + agentInstructions: [ + agentInstructionFile('/somewhere/AGENTS.md'), + ], + }); + const workspaceService = createMockWorkspaceService({ activeRoot: undefined }); + const contextService = createMockWorkspaceContextService([]); + + const counts = await getSourceCounts( + promptsService, PromptsType.instructions, + { sources: [PromptsStorage.local, PromptsStorage.user] }, + contextService, workspaceService, + ); + + // No workspace context → classified as user + assert.strictEqual(counts.workspace, 0); + assert.strictEqual(counts.user, 1); + }); + }); +}); diff --git a/src/vs/sessions/test/ai-customizations.test.md b/src/vs/sessions/test/ai-customizations.test.md new file mode 100644 index 0000000000000..acc5ec8d364bf --- /dev/null +++ b/src/vs/sessions/test/ai-customizations.test.md @@ -0,0 +1,166 @@ +# AI Customizations Test Plan + +The following test plan outlines the scenarios and specifications for the AI Customizations feature, which includes a management editor and tree view for managing customization items. + +## SPECS + +- [`../AI_CUSTOMIZATIONS.md`](../AI_CUSTOMIZATIONS.md) + +## SCENARIOS + +### Scenario 1: Empty state — no session, no customizations + +#### Preconditions + +- On 'New Session' screen +- No folder selected +- No user customizations created (from this tool or others i.e. Copilot CLI) + +#### Actions + +1. Open the sidebar customizations section +2. Observe no sidebar counts are shown for any section (Agents, Skills, Instructions, Prompts, Hooks) +3. Open the management editor by clicking on one of the sections (e.g., "Instructions") +4. Observe the empty state message and +4. Click through each section in the sidebar + +#### Expected Results + +- All sidebar counts show 0 (no badges visible) +- Management editor shows empty state for each section with "No X yet" message and create +- "Add" button is disabled or hidden (no active workspace to create in) +- User storage group is empty (no `~/.copilot/` or `~/.claude/` files) +- Extension storage group may show built-in extension items if Copilot extension contributes any + +#### Notes + +- This tests the baseline empty state before any session or workspace is active +- The `isSessionsWindow` flag should be `true`, verified via Developer: Customizations Debug + +--- + +### Scenario 2: Active session with workspace customizations + +#### Preconditions + +- Active session with a repository that has customizations (e.g., the `vscode` repo which has `.github/agents/`, `.github/skills/`, `.github/instructions/`, `.github/prompts/`) +- Session has a worktree checked out + +#### Actions + +1. Observe sidebar counts update after session becomes active +2. Open management editor, select "Instructions" section +3. Verify items listed match the workspace files +4. Run Developer: Customizations Debug and compare Stage 1 (raw data) with Stage 3 (widget state) +5. Select "Agents" section and verify agent count +6. Select "Prompts" section and verify skill-type commands are excluded + +#### Expected Results + +- Sidebar counts match editor list counts for every section +- Instructions section includes AGENTS.md, CLAUDE.md, copilot-instructions.md (from `listAgentInstructions`) classified as "Workspace" +- Instructions section includes `.instructions.md` files from `.github/instructions/` +- Agents section uses `getCustomAgents()` — parsed names, not raw filenames (README.md excluded) +- Prompts section shows only prompt-type commands, not skill-type (skills have their own section) +- Hooks section shows only workspace-local hook files (user hooks filtered out by `sources: [local]`) +- Debug report shows `Window: Sessions`, `Active root: /path/to/worktree` +- Extension and plugin groups are not shown (sessions filter: `sources: [local, user]`) + +#### Notes + +- This is the core "happy path" for sessions — most customizations come from the workspace +- Count consistency between sidebar badges and editor item count is the key regression test + +--- + +### Scenario 3: User-level customizations from CLI paths + +#### Preconditions + +- Files exist in `~/.copilot/instructions/`, `~/.claude/rules/`, or `~/.claude/agents/` +- Active session with a repository open + +#### Actions + +1. Open management editor, select "Instructions" +2. Verify user-level instruction files appear under the "User" group +3. Select "Agents" section and check for `~/.claude/agents/` user agents +4. Run Developer: Customizations Debug and check the `includedUserFileRoots` filter +5. Verify that VS Code profile user files (e.g., `$PROFILE/instructions/`) are NOT shown + +#### Expected Results + +- User files from `~/.copilot/` and `~/.claude/` appear in the "User" group +- User files from the VS Code profile path do NOT appear (filtered by `includedUserFileRoots: [~/.copilot, ~/.claude, ~/.agents]`) +- Prompts section shows ALL user roots (filter has `includedUserFileRoots: undefined`) — including VS Code profile prompts +- Debug report Stage 2 shows "Removed" entries for any filtered-out user files +- Sidebar counts reflect the filtered user file counts + +#### Notes + +- This validates the `IStorageSourceFilter.includedUserFileRoots` allowlist +- Prompts are intentionally an exception — they show from all user roots since CLI now supports user prompts + +--- + +### Scenario 4: Creating new customization files + +#### Preconditions + +- Active session with a worktree + +#### Actions + +1. Open management editor, select "Instructions" +2. Click the "Add" button → "New Instructions (Workspace)" +3. Verify the file is created in `.github/instructions/` under the worktree +4. Click "Add" button dropdown → "New Instructions (User)" +5. Verify the file is created in `~/.copilot/instructions/` (not VS Code profile) +6. Select "Hooks" section +7. Click "Add" → verify a `hooks.json` is created in `.github/hooks/` +8. Verify the hooks.json has `"version": 1`, uses `"bash"` field, and contains all events from `COPILOT_CLI_HOOK_TYPE_MAP` + +#### Expected Results + +- Workspace files created under the active worktree's `.github/` folder +- User files created under `~/.copilot/{type}/` (from `AgenticPromptsService.getSourceFolders()` override) +- Hooks.json skeleton has correct Copilot CLI format: `version: 1`, `bash` (not `command`), all hook events derived from schema +- After creation, item count updates automatically +- Created files are editable in the embedded editor + +#### Notes + +- This tests that `AgenticPromptsService.getSourceFolders()` correctly redirects user creation to `~/.copilot/` +- Hooks creation derives events from `COPILOT_CLI_HOOK_TYPE_MAP` — adding new events to the schema auto-includes them + +--- + +### Scenario 5: Switching sessions updates customizations + +#### Preconditions + +- Two sessions active: one with a repo that has many customizations, another with none +- Or: one active session, then start a new session with a different repo + +#### Actions + +1. Note the sidebar customization counts for the first session +2. Switch to the second session (click it in the session list) +3. Observe sidebar counts update +4. Open the management editor and verify items reflect the new session's workspace +5. Switch back to the first session +6. Verify counts and items revert to the first session's state + +#### Expected Results + +- Sidebar counts update reactively when `activeSession` observable changes +- Management editor items refresh automatically (list widget subscribes to `activeProjectRoot`) +- Active root in the debug report changes to the new session's worktree +- No stale counts from the previous session persist +- If the new session has no worktree, counts show only user-level items (workspace = 0) +- "Add (Workspace)" button becomes disabled when no active root + +#### Notes + +- This tests the reactive wiring: `autorun` on `activeSession` triggers `_updateCounts()` in toolbar and `refresh()` in list widget +- Stale count bugs typically manifest when switching sessions — the count remains from the prior session diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts new file mode 100644 index 0000000000000..e4ed34928ff12 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IPromptsService, PromptsStorage, IPromptPath } from '../../common/promptSyntax/service/promptsService.js'; +import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js'; +import { AICustomizationManagementSection } from './aiCustomizationManagement.js'; + +/** + * Maps section ID to prompt type. Duplicated from aiCustomizationListWidget + * to avoid a circular dependency. + */ +function sectionToPromptType(section: AICustomizationManagementSection): PromptsType { + switch (section) { + case AICustomizationManagementSection.Agents: + return PromptsType.agent; + case AICustomizationManagementSection.Skills: + return PromptsType.skill; + case AICustomizationManagementSection.Instructions: + return PromptsType.instructions; + case AICustomizationManagementSection.Hooks: + return PromptsType.hook; + case AICustomizationManagementSection.Prompts: + default: + return PromptsType.prompt; + } +} + +/** + * Snapshot of the list widget's internal state, passed in to avoid coupling. + */ +export interface IDebugWidgetState { + readonly allItems: readonly { readonly storage: PromptsStorage }[]; + readonly displayEntries: readonly { type: string; label?: string; count?: number; collapsed?: boolean }[]; +} + +/** + * Generates a debug diagnostics report for the AI Customization list widget. + * Returns the report as a string suitable for opening in an editor. + */ +export async function generateCustomizationDebugReport( + section: AICustomizationManagementSection, + promptsService: IPromptsService, + workspaceService: IAICustomizationWorkspaceService, + widgetState: IDebugWidgetState, +): Promise { + const promptType = sectionToPromptType(section); + const filter = workspaceService.getStorageSourceFilter(promptType); + const lines: string[] = []; + + lines.push(`== Customization Debug: ${section} (${promptType}) ==`); + lines.push(`Window: ${workspaceService.isSessionsWindow ? 'Sessions' : 'Core VS Code'}`); + lines.push(`Active root: ${workspaceService.getActiveProjectRoot()?.fsPath ?? '(none)'}`); + lines.push(`Sections: [${workspaceService.managementSections.join(', ')}]`); + lines.push(`Filter sources: [${filter.sources.join(', ')}]`); + if (filter.includedUserFileRoots) { + lines.push(`Filter includedUserFileRoots:`); + for (const r of filter.includedUserFileRoots) { + lines.push(` ${r.fsPath}`); + } + } else { + lines.push(`Filter includedUserFileRoots: (all)`); + } + lines.push(''); + + await appendRawServiceData(lines, promptsService, promptType); + await appendFilteredData(lines, promptsService, promptType, filter); + appendWidgetState(lines, widgetState); + await appendSourceFolders(lines, promptsService, promptType); + + return lines.join('\n'); +} + +async function appendRawServiceData(lines: string[], promptsService: IPromptsService, promptType: PromptsType): Promise { + lines.push('--- Stage 1: Raw PromptsService Data ---'); + + const [localFiles, userFiles, extensionFiles] = await Promise.all([ + promptsService.listPromptFilesForStorage(promptType, PromptsStorage.local, CancellationToken.None), + promptsService.listPromptFilesForStorage(promptType, PromptsStorage.user, CancellationToken.None), + promptsService.listPromptFilesForStorage(promptType, PromptsStorage.extension, CancellationToken.None), + ]); + + lines.push(` listPromptFilesForStorage(local): ${localFiles.length} files`); + appendFileList(lines, localFiles); + + lines.push(` listPromptFilesForStorage(user): ${userFiles.length} files`); + appendFileList(lines, userFiles); + + lines.push(` listPromptFilesForStorage(ext): ${extensionFiles.length} files`); + appendFileList(lines, extensionFiles); + + const allFiles = await promptsService.listPromptFiles(promptType, CancellationToken.None); + lines.push(` listPromptFiles (merged): ${allFiles.length} files`); + + if (promptType === PromptsType.instructions) { + const agentInstructions = await promptsService.listAgentInstructions(CancellationToken.None, undefined); + lines.push(` listAgentInstructions (extra): ${agentInstructions.length} files`); + appendFileList(lines, agentInstructions); + } + + if (promptType === PromptsType.skill) { + const skills = await promptsService.findAgentSkills(CancellationToken.None); + lines.push(` findAgentSkills: ${skills?.length ?? 0} skills`); + for (const s of skills ?? []) { + lines.push(` ${s.name ?? '?'} [${s.storage}] ${s.uri.fsPath}`); + } + } + + if (promptType === PromptsType.agent) { + const agents = await promptsService.getCustomAgents(CancellationToken.None); + lines.push(` getCustomAgents: ${agents.length} agents`); + for (const a of agents) { + lines.push(` ${a.name} [${a.source.storage}] ${a.uri.fsPath}`); + } + } + + if (promptType === PromptsType.prompt) { + const commands = await promptsService.getPromptSlashCommands(CancellationToken.None); + lines.push(` getPromptSlashCommands: ${commands.length} commands`); + for (const c of commands) { + lines.push(` /${c.name} [${c.promptPath.storage}] ${c.promptPath.uri.fsPath} (type=${c.promptPath.type})`); + } + } + + lines.push(''); +} + +async function appendFilteredData(lines: string[], promptsService: IPromptsService, promptType: PromptsType, filter: IStorageSourceFilter): Promise { + lines.push('--- Stage 2: After applyStorageSourceFilter ---'); + + const [localFiles, userFiles, extensionFiles] = await Promise.all([ + promptsService.listPromptFilesForStorage(promptType, PromptsStorage.local, CancellationToken.None), + promptsService.listPromptFilesForStorage(promptType, PromptsStorage.user, CancellationToken.None), + promptsService.listPromptFilesForStorage(promptType, PromptsStorage.extension, CancellationToken.None), + ]); + + const all: IPromptPath[] = [...localFiles, ...userFiles, ...extensionFiles]; + const filtered = applyStorageSourceFilter(all, filter); + lines.push(` Input: ${all.length} → Filtered: ${filtered.length}`); + lines.push(` local: ${filtered.filter(f => f.storage === PromptsStorage.local).length}`); + lines.push(` user: ${filtered.filter(f => f.storage === PromptsStorage.user).length}`); + lines.push(` extension: ${filtered.filter(f => f.storage === PromptsStorage.extension).length}`); + + const removedCount = all.length - filtered.length; + if (removedCount > 0) { + const filteredUris = new Set(filtered.map(f => f.uri.toString())); + const removed = all.filter(f => !filteredUris.has(f.uri.toString())); + lines.push(` Removed (${removedCount}):`); + for (const f of removed) { + lines.push(` [${f.storage}] ${f.uri.fsPath}`); + } + } + + lines.push(''); +} + +function appendWidgetState(lines: string[], state: IDebugWidgetState): void { + lines.push('--- Stage 3: Widget State (loadItems → filterItems) ---'); + lines.push(` allItems (after loadItems): ${state.allItems.length}`); + lines.push(` local: ${state.allItems.filter(i => i.storage === PromptsStorage.local).length}`); + lines.push(` user: ${state.allItems.filter(i => i.storage === PromptsStorage.user).length}`); + lines.push(` extension: ${state.allItems.filter(i => i.storage === PromptsStorage.extension).length}`); + lines.push(` plugin: ${state.allItems.filter(i => i.storage === PromptsStorage.plugin).length}`); + + lines.push(` displayEntries (after filterItems): ${state.displayEntries.length}`); + const fileEntries = state.displayEntries.filter(e => e.type === 'file-item'); + lines.push(` file items shown: ${fileEntries.length}`); + const groupEntries = state.displayEntries.filter(e => e.type === 'group-header'); + for (const g of groupEntries) { + lines.push(` group "${g.label}": count=${g.count}, collapsed=${g.collapsed}`); + } + lines.push(''); +} + +async function appendSourceFolders(lines: string[], promptsService: IPromptsService, promptType: PromptsType): Promise { + lines.push('--- Source Folders (creation targets) ---'); + const sourceFolders = await promptsService.getSourceFolders(promptType); + for (const sf of sourceFolders) { + lines.push(` [${sf.storage}] ${sf.uri.fsPath}`); + } + + try { + const resolvedFolders = await promptsService.getResolvedSourceFolders(promptType); + lines.push(''); + lines.push('--- Resolved Source Folders (discovery order) ---'); + for (const rf of resolvedFolders) { + lines.push(` [${rf.storage}] ${rf.uri.fsPath} (source=${rf.source})`); + } + } catch { + // getResolvedSourceFolders may not exist for all types + } +} + +function appendFileList(lines: string[], files: readonly { uri: URI }[]): void { + for (const f of files) { + lines.push(` ${f.uri.fsPath}`); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 4d240a6a64a6e..a0c135aacc637 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -34,12 +34,12 @@ import { IContextKeyService } from '../../../../../platform/contextkey/common/co import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IAICustomizationWorkspaceService, applyStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js'; import { Action, Separator } from '../../../../../base/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ISCMService } from '../../../scm/common/scm.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { generateCustomizationDebugReport } from './aiCustomizationDebugPanel.js'; const $ = DOM.$; @@ -364,7 +364,6 @@ export class AICustomizationListWidget extends Disposable { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ILabelService private readonly labelService: ILabelService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, - @ILogService private readonly logService: ILogService, @IClipboardService private readonly clipboardService: IClipboardService, @ISCMService private readonly scmService: ISCMService, @IHoverService private readonly hoverService: IHoverService, @@ -621,7 +620,7 @@ export class AICustomizationListWidget extends Disposable { this.addButton.element.style.display = hasDropdown ? '' : 'none'; this.addButtonSimple.element.style.display = hasDropdown ? 'none' : ''; - if (this.workspaceService.preferManualCreation) { + if (this.workspaceService.isSessionsWindow) { // Sessions: primary is workspace creation const hasWorkspace = this.hasActiveWorkspace(); const label = `$(${Codicon.add.id}) New ${typeLabel} (Workspace)`; @@ -672,7 +671,7 @@ export class AICustomizationListWidget extends Disposable { // Hooks: no user-scoped creation if (promptType === PromptsType.hook) { - if (this.workspaceService.preferManualCreation) { + if (this.workspaceService.isSessionsWindow) { // Sessions: no dropdown for hooks } else { // Core: primary is generate, dropdown has configure quick pick @@ -685,7 +684,7 @@ export class AICustomizationListWidget extends Disposable { return actions; } - if (this.workspaceService.preferManualCreation) { + if (this.workspaceService.isSessionsWindow) { // Sessions: primary is workspace, dropdown has user actions.push(this.dropdownActionDisposables.add(new Action('createUser', `$(${Codicon.account.id}) New ${typeLabel} (User)`, undefined, true, () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'user' }); @@ -717,7 +716,7 @@ export class AICustomizationListWidget extends Disposable { */ private executePrimaryCreateAction(): void { const promptType = sectionToPromptType(this.currentSection); - if (this.workspaceService.preferManualCreation) { + if (this.workspaceService.isSessionsWindow) { // Sessions: primary creates in workspace if (!this.hasActiveWorkspace()) { return; @@ -763,10 +762,6 @@ export class AICustomizationListWidget extends Disposable { const promptType = sectionToPromptType(this.currentSection); const items: IAICustomizationListItem[] = []; - const folders = this.workspaceContextService.getWorkspace().folders; - const activeRepo = this.workspaceService.getActiveProjectRoot(); - this.logService.info(`[AICustomizationListWidget] loadItems: section=${this.currentSection}, promptType=${promptType}, workspaceFolders=[${folders.map(f => f.uri.toString()).join(', ')}], activeRepo=${activeRepo?.toString() ?? 'none'}`); - if (promptType === PromptsType.agent) { // Use getCustomAgents which has parsed name/description from frontmatter @@ -882,15 +877,11 @@ export class AICustomizationListWidget extends Disposable { items.push(...pluginItems.map(mapToListItem)); } - // Filter out files under excluded user roots - const excludedRoots = this.workspaceService.excludedUserFileRoots; - if (excludedRoots.length > 0) { - for (let i = items.length - 1; i >= 0; i--) { - if (items[i].storage === PromptsStorage.user && excludedRoots.some(root => isEqualOrParent(items[i].uri, root))) { - items.splice(i, 1); - } - } - } + // Apply storage source filter (removes items not in visible sources or excluded user roots) + const filter = this.workspaceService.getStorageSourceFilter(promptType); + const filteredItems = applyStorageSourceFilter(items, filter); + items.length = 0; + items.push(...filteredItems); // Sort items by name items.sort((a, b) => a.name.localeCompare(b.name)); @@ -898,8 +889,6 @@ export class AICustomizationListWidget extends Disposable { // Set git status for workspace (local) items this.updateGitStatus(items); - this.logService.info(`[AICustomizationListWidget] loadItems complete: ${items.length} items loaded [${items.map(i => `${i.name}(${i.storage}:${i.uri.toString()})`).join(', ')}]`); - this.allItems = items; this.filterItems(); this._onDidChangeItemCount.fire(items.length); @@ -973,12 +962,9 @@ export class AICustomizationListWidget extends Disposable { } } - const totalBeforeFilter = matchedItems.length; - this.logService.info(`[AICustomizationListWidget] filterItems: allItems=${this.allItems.length}, matched=${totalBeforeFilter}`); - // Group items by storage const promptType = sectionToPromptType(this.currentSection); - const visibleSources = new Set(this.workspaceService.getVisibleStorageSources(promptType)); + const visibleSources = new Set(this.workspaceService.getStorageSourceFilter(promptType).sources); const groups: { storage: PromptsStorage; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = [ { storage: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, { storage: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, @@ -1029,7 +1015,6 @@ export class AICustomizationListWidget extends Disposable { } this.list.splice(0, this.list.length, this.displayEntries); - this.logService.info(`[AICustomizationListWidget] filterItems complete: ${this.displayEntries.length} display entries spliced into list`); this.updateEmptyState(); } @@ -1170,4 +1155,16 @@ export class AICustomizationListWidget extends Disposable { get itemCount(): number { return this.allItems.length; } + + /** + * Generates a debug report for the current section. + */ + async generateDebugReport(): Promise { + return generateCustomizationDebugReport( + this.currentSection, + this.promptsService, + this.workspaceService, + { allItems: this.allItems, displayEntries: this.displayEntries }, + ); + } } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts index 5e95b733bbbfb..0cd2c243b0f75 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts @@ -6,6 +6,7 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -270,8 +271,8 @@ class AICustomizationManagementActionsContribution extends Disposable implements constructor() { super({ id: AICustomizationManagementCommands.OpenEditor, - title: localize2('openAICustomizations', "Open Chat Customizations (Preview)"), - shortTitle: localize2('aiCustomizations', "Chat Customizations (Preview)"), + title: localize2('openAICustomizations', "Open Customizations (Preview)"), + shortTitle: localize2('aiCustomizations', "Customizations (Preview)"), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.has(`config.${ChatConfiguration.ChatCustomizationMenuEnabled}`)), f1: true, @@ -287,6 +288,27 @@ class AICustomizationManagementActionsContribution extends Disposable implements } } })); + + // Toggle Debug Panel in AI Customizations Editor + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: AICustomizationManagementCommands.ToggleDebug, + title: localize2('toggleDebugPanel', "Customizations Debug"), + category: Categories.Developer, + f1: true, + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const pane = editorService.activeEditorPane; + if (pane instanceof AICustomizationManagementEditor) { + const report = await (pane as AICustomizationManagementEditor).generateDebugReport(); + await editorService.openEditor({ resource: undefined, contents: report, options: { pinned: false } }); + } + } + })); } } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts index 6aa4d4ba4d467..55da7d7eb798d 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts @@ -26,6 +26,7 @@ export const AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID = 'workbench.input.aiCu */ export const AICustomizationManagementCommands = { OpenEditor: 'aiCustomization.openManagementEditor', + ToggleDebug: 'aiCustomization.toggleDebugPanel', CreateNewAgent: 'aiCustomization.createNewAgent', CreateNewSkill: 'aiCustomization.createNewSkill', CreateNewInstructions: 'aiCustomization.createNewInstructions', diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 1aa934879ae94..8177097058f42 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -60,10 +60,10 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditorOptions.js'; import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; -import { IPathService } from '../../../../services/path/common/pathService.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; import { HOOKS_SOURCE_FOLDER } from '../../common/promptSyntax/config/promptFileLocations.js'; +import { COPILOT_CLI_HOOK_TYPE_MAP } from '../../common/promptSyntax/hookSchema.js'; import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput.js'; import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js'; import { IWorkbenchMcpServer } from '../../../mcp/common/mcpTypes.js'; @@ -185,7 +185,6 @@ export class AICustomizationManagementEditor extends EditorPane { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @ITextFileService private readonly textFileService: ITextFileService, - @IPathService private readonly pathService: IPathService, @IFileService private readonly fileService: IFileService, ) { super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService); @@ -505,7 +504,7 @@ export class AICustomizationManagementEditor extends EditorPane { private async createNewItemManual(type: PromptsType, target: 'workspace' | 'user'): Promise { if (type === PromptsType.hook) { - if (this.workspaceService.preferManualCreation) { + if (this.workspaceService.isSessionsWindow) { // Sessions: directly create a Copilot CLI format hooks file await this.createCopilotCliHookFile(); } else { @@ -522,7 +521,7 @@ export class AICustomizationManagementEditor extends EditorPane { const targetDir = target === 'workspace' ? resolveWorkspaceTargetDirectory(this.workspaceService, type) - : await resolveUserTargetDirectory(this.promptsService, type, this.configurationService, this.pathService); + : await resolveUserTargetDirectory(this.promptsService, type); const options: INewPromptOptions = { targetFolder: targetDir, @@ -563,22 +562,12 @@ export class AICustomizationManagementEditor extends EditorPane { try { await this.fileService.stat(hookFileUri); } catch { - const hooksContent = { - hooks: { - sessionStart: [ - { type: 'command', command: '' } - ], - userPromptSubmitted: [ - { type: 'command', command: '' } - ], - preToolUse: [ - { type: 'command', command: '' } - ], - postToolUse: [ - { type: 'command', command: '' } - ], - } - }; + // Derive hook event names from the schema so new events are automatically included + const hooks: Record = {}; + for (const eventName of Object.keys(COPILOT_CLI_HOOK_TYPE_MAP)) { + hooks[eventName] = [{ type: 'command', bash: '' }]; + } + const hooksContent = { version: 1, hooks }; const jsonContent = JSON.stringify(hooksContent, null, '\t'); await this.fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent)); } @@ -658,6 +647,13 @@ export class AICustomizationManagementEditor extends EditorPane { void this.listWidget.refresh(); } + /** + * Generates a debug report for the current section. + */ + public async generateDebugReport(): Promise { + return this.listWidget.generateDebugReport(); + } + //#region Embedded Editor private createEmbeddedEditor(): void { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 50a5f6acc17c9..0f22c638e180b 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -6,7 +6,7 @@ import { derived, IObservable, observableFromEventOpts } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IAICustomizationWorkspaceService, AICustomizationManagementSection } from '../../common/aiCustomizationWorkspaceService.js'; +import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js'; import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -53,20 +53,15 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic AICustomizationManagementSection.McpServers, ]; - readonly visibleStorageSources: readonly PromptsStorage[] = [ - PromptsStorage.local, - PromptsStorage.user, - PromptsStorage.extension, - PromptsStorage.plugin, - ]; + private static readonly _defaultFilter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension, PromptsStorage.plugin], + }; - getVisibleStorageSources(_type: PromptsType): readonly PromptsStorage[] { - return this.visibleStorageSources; + getStorageSourceFilter(_type: PromptsType): IStorageSourceFilter { + return AICustomizationWorkspaceService._defaultFilter; } - readonly preferManualCreation = false; - - readonly excludedUserFileRoots: readonly URI[] = []; + readonly isSessionsWindow = false; async commitFiles(_projectRoot: URI, _fileUris: URI[]): Promise { // No-op in core VS Code. diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts index 00d0721d35bcf..7058da4c373e9 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts @@ -6,15 +6,13 @@ import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; import { IChatWidgetService } from '../chat.js'; import { IChatService } from '../../common/chatService/chatService.js'; -import { ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { ChatModeKind } from '../../common/constants.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { getPromptFileDefaultLocations } from '../../common/promptSyntax/config/promptFileLocations.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { IPathService } from '../../../../services/path/common/pathService.js'; import { localize } from '../../../../../nls.js'; /** @@ -34,8 +32,6 @@ export class CustomizationCreatorService { @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @IPromptsService private readonly promptsService: IPromptsService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IPathService private readonly pathService: IPathService, ) { } async createWithAI(type: PromptsType): Promise { @@ -103,7 +99,7 @@ export class CustomizationCreatorService { * Resolves the user-level directory for a new customization file. */ async resolveUserDirectory(type: PromptsType): Promise { - return resolveUserTargetDirectory(this.promptsService, type, this.configurationService, this.pathService); + return resolveUserTargetDirectory(this.promptsService, type); } } @@ -125,41 +121,18 @@ export function resolveWorkspaceTargetDirectory(workspaceService: IAICustomizati /** * Resolves the user-level directory for a new customization file. - * If chat.customizations.userStoragePath is set, uses that as the base - * with a type-appropriate subfolder. Otherwise falls back to the default - * source folders from IPromptsService. + * Delegates to IPromptsService.getSourceFolders() which returns the appropriate + * user root (VS Code profile in core, ~/.copilot in sessions). */ export async function resolveUserTargetDirectory( promptsService: IPromptsService, type: PromptsType, - configurationService?: IConfigurationService, - pathService?: IPathService, ): Promise { - const overridePath = configurationService?.getValue(ChatConfiguration.ChatCustomizationUserStoragePath); - const subfolder = getUserStorageSubfolder(type); - if (overridePath && pathService && subfolder) { - const userHome = await pathService.userHome(); - const resolved = overridePath.startsWith('~/') - ? URI.joinPath(userHome, overridePath.slice(2)) - : URI.file(overridePath); - return URI.joinPath(resolved, subfolder); - } const folders = await promptsService.getSourceFolders(type); const userFolder = folders.find(f => f.storage === PromptsStorage.user); return userFolder?.uri; } -/** - * Returns the subdirectory name for user-level storage of a given prompt type. - */ -function getUserStorageSubfolder(type: PromptsType): string | undefined { - switch (type) { - case PromptsType.instructions: return 'instructions'; - case PromptsType.skill: return 'skills'; - default: return undefined; - } -} - //#region Agent Instructions /** diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index a73f03cf41b08..3d8d31df97f53 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1248,12 +1248,6 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.aiCustomizationMenu.enabled', "Controls whether the Chat Customizations editor is available in the Command Palette. When disabled, the Chat Customizations editor and related commands are hidden."), default: true, }, - [ChatConfiguration.ChatCustomizationUserStoragePath]: { - type: 'string', - tags: ['experimental'], - description: nls.localize('chat.customizationsMenu.userStoragePath', "Experimental: This setting is temporary and should not be relied on. Override the base directory for user-level customization files. When set, new user customizations are created here instead of the VS Code profile folder."), - default: '', - } } }); Registry.as(EditorExtensions.EditorPane).registerEditorPane( diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index 93de4ba4d710d..28467684c8d93 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -5,6 +5,7 @@ import { IObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; +import { isEqualOrParent } from '../../../../base/common/resources.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { PromptsType } from './promptSyntax/promptTypes.js'; import { PromptsStorage } from './promptSyntax/service/promptsService.js'; @@ -26,6 +27,41 @@ export const AICustomizationManagementSection = { export type AICustomizationManagementSection = typeof AICustomizationManagementSection[keyof typeof AICustomizationManagementSection]; +/** + * Per-type filter policy controlling which storage sources and user file + * roots are visible for a given customization type. + */ +export interface IStorageSourceFilter { + /** + * Which storage groups to display (e.g. workspace, user, extension). + */ + readonly sources: readonly PromptsStorage[]; + + /** + * If set, only user files under these roots are shown (allowlist). + * If `undefined`, all user file roots are included. + */ + readonly includedUserFileRoots?: readonly URI[]; +} + +/** + * Applies a storage source filter to an array of items that have uri and storage. + * Removes items whose storage is not in the filter's source list, + * and for user-storage items, removes those not under an allowed root. + */ +export function applyStorageSourceFilter(items: readonly T[], filter: IStorageSourceFilter): readonly T[] { + const sourceSet = new Set(filter.sources); + return items.filter(item => { + if (!sourceSet.has(item.storage)) { + return false; + } + if (item.storage === PromptsStorage.user && filter.includedUserFileRoots) { + return filter.includedUserFileRoots.some(root => isEqualOrParent(item.uri, root)); + } + return true; + }); +} + /** * Provides workspace context for AI Customization views. */ @@ -48,26 +84,15 @@ export interface IAICustomizationWorkspaceService { readonly managementSections: readonly AICustomizationManagementSection[]; /** - * The storage sources to show as groups in the customization list. - */ - readonly visibleStorageSources: readonly PromptsStorage[]; - - /** - * Returns the visible storage sources for a specific customization type. - * Allows per-type overrides (e.g., hooks may only show workspace sources). - */ - getVisibleStorageSources(type: PromptsType): readonly PromptsStorage[]; - - /** - * URI roots to exclude from user-level file listings. - * Files under these roots are hidden from the customization list. + * Returns the storage source filter for a given customization type. + * Controls which storage groups and user file roots are visible. */ - readonly excludedUserFileRoots: readonly URI[]; + getStorageSourceFilter(type: PromptsType): IStorageSourceFilter; /** - * Whether the primary creation action should create a file directly + * Whether this is a sessions window (vs core VS Code). */ - readonly preferManualCreation: boolean; + readonly isSessionsWindow: boolean; /** * Commits files in the active project. diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index aa9dcb530a70e..d7ce75b61bd31 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -56,7 +56,6 @@ export enum ChatConfiguration { ExplainChangesEnabled = 'chat.editing.explainChanges.enabled', GrowthNotificationEnabled = 'chat.growthNotification.enabled', ChatCustomizationMenuEnabled = 'chat.customizationsMenu.enabled', - ChatCustomizationUserStoragePath = 'chat.customizationsMenu.userStoragePath', } /** diff --git a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/applyStorageSourceFilter.test.ts b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/applyStorageSourceFilter.test.ts new file mode 100644 index 0000000000000..95988b49868b5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/applyStorageSourceFilter.test.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; +import { applyStorageSourceFilter, IStorageSourceFilter } from '../../../common/aiCustomizationWorkspaceService.js'; + +function item(path: string, storage: PromptsStorage): { uri: URI; storage: PromptsStorage } { + return { uri: URI.file(path), storage }; +} + +suite('applyStorageSourceFilter', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('source filtering', () => { + test('keeps items matching sources', () => { + const items = [ + item('/w/a.md', PromptsStorage.local), + item('/u/b.md', PromptsStorage.user), + item('/e/c.md', PromptsStorage.extension), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension], + }; + assert.strictEqual(applyStorageSourceFilter(items, filter).length, 3); + }); + + test('removes items not in sources', () => { + const items = [ + item('/w/a.md', PromptsStorage.local), + item('/u/b.md', PromptsStorage.user), + item('/e/c.md', PromptsStorage.extension), + item('/p/d.md', PromptsStorage.plugin), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local], + }; + const result = applyStorageSourceFilter(items, filter); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].uri.toString(), URI.file('/w/a.md').toString()); + }); + + test('empty sources removes everything', () => { + const items = [ + item('/w/a.md', PromptsStorage.local), + item('/u/b.md', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { sources: [] }; + assert.strictEqual(applyStorageSourceFilter(items, filter).length, 0); + }); + + test('empty items returns empty', () => { + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user], + }; + assert.strictEqual(applyStorageSourceFilter([], filter).length, 0); + }); + }); + + suite('includedUserFileRoots filtering', () => { + test('undefined includedUserFileRoots keeps all user files', () => { + const items = [ + item('/home/.copilot/a.md', PromptsStorage.user), + item('/home/.vscode/b.md', PromptsStorage.user), + item('/home/.claude/c.md', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.user], + // includedUserFileRoots not set = allow all + }; + assert.strictEqual(applyStorageSourceFilter(items, filter).length, 3); + }); + + test('includedUserFileRoots filters user files by root', () => { + const items = [ + item('/home/.copilot/instructions/a.md', PromptsStorage.user), + item('/home/.vscode/instructions/b.md', PromptsStorage.user), + item('/home/.claude/rules/c.md', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.user], + includedUserFileRoots: [URI.file('/home/.copilot'), URI.file('/home/.claude')], + }; + const result = applyStorageSourceFilter(items, filter); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].uri.toString(), URI.file('/home/.copilot/instructions/a.md').toString()); + assert.strictEqual(result[1].uri.toString(), URI.file('/home/.claude/rules/c.md').toString()); + }); + + test('includedUserFileRoots does not affect non-user items', () => { + const items = [ + item('/w/a.md', PromptsStorage.local), + item('/e/b.md', PromptsStorage.extension), + item('/home/.copilot/c.md', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.extension, PromptsStorage.user], + includedUserFileRoots: [URI.file('/home/.copilot')], + }; + const result = applyStorageSourceFilter(items, filter); + // local + extension kept (not affected by user root filter), user kept (matches root) + assert.strictEqual(result.length, 3); + }); + + test('empty includedUserFileRoots removes all user files', () => { + const items = [ + item('/w/a.md', PromptsStorage.local), + item('/home/.copilot/b.md', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user], + includedUserFileRoots: [], // explicit empty = no user files allowed + }; + const result = applyStorageSourceFilter(items, filter); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].storage, PromptsStorage.local); + }); + + test('user file at exact root is included', () => { + const items = [ + item('/home/.copilot', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.user], + includedUserFileRoots: [URI.file('/home/.copilot')], + }; + assert.strictEqual(applyStorageSourceFilter(items, filter).length, 1); + }); + + test('user file outside all roots is excluded', () => { + const items = [ + item('/other/path/a.md', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.user], + includedUserFileRoots: [URI.file('/home/.copilot'), URI.file('/home/.claude')], + }; + assert.strictEqual(applyStorageSourceFilter(items, filter).length, 0); + }); + + test('deeply nested user file under root is included', () => { + const items = [ + item('/home/.copilot/instructions/sub/deep/a.md', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.user], + includedUserFileRoots: [URI.file('/home/.copilot')], + }; + assert.strictEqual(applyStorageSourceFilter(items, filter).length, 1); + }); + }); + + suite('combined filtering', () => { + test('source filter + user root filter applied together', () => { + const items = [ + item('/w/a.md', PromptsStorage.local), + item('/home/.copilot/b.md', PromptsStorage.user), + item('/home/.vscode/c.md', PromptsStorage.user), + item('/e/d.md', PromptsStorage.extension), + item('/p/e.md', PromptsStorage.plugin), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user], + includedUserFileRoots: [URI.file('/home/.copilot')], + }; + const result = applyStorageSourceFilter(items, filter); + // local (kept), .copilot user (kept), .vscode user (excluded by root), + // extension (excluded by source), plugin (excluded by source) + assert.strictEqual(result.length, 2); + }); + + test('sessions-like filter: hooks show only local', () => { + const items = [ + item('/w/.github/hooks/pre.json', PromptsStorage.local), + item('/home/.claude/settings.json', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local], + }; + const result = applyStorageSourceFilter(items, filter); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].storage, PromptsStorage.local); + }); + + test('sessions-like filter: instructions show only CLI roots', () => { + const items = [ + item('/w/.github/instructions/a.md', PromptsStorage.local), + item('/home/.copilot/instructions/b.md', PromptsStorage.user), + item('/home/.claude/rules/c.md', PromptsStorage.user), + item('/home/.vscode-profile/instructions/d.md', PromptsStorage.user), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user], + includedUserFileRoots: [ + URI.file('/home/.copilot'), + URI.file('/home/.claude'), + URI.file('/home/.agents'), + ], + }; + const result = applyStorageSourceFilter(items, filter); + // local + .copilot + .claude pass; .vscode-profile excluded + assert.strictEqual(result.length, 3); + }); + + test('core-like filter: show everything', () => { + const items = [ + item('/w/a.md', PromptsStorage.local), + item('/u/b.md', PromptsStorage.user), + item('/e/c.md', PromptsStorage.extension), + item('/p/d.md', PromptsStorage.plugin), + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension, PromptsStorage.plugin], + }; + assert.strictEqual(applyStorageSourceFilter(items, filter).length, 4); + }); + }); + + suite('type safety', () => { + test('works with objects that have extra properties', () => { + const items = [ + { uri: URI.file('/w/a.md'), storage: PromptsStorage.local, name: 'A', extra: true }, + { uri: URI.file('/u/b.md'), storage: PromptsStorage.user, name: 'B', extra: false }, + ]; + const filter: IStorageSourceFilter = { + sources: [PromptsStorage.local], + }; + const result = applyStorageSourceFilter(items, filter); + assert.strictEqual(result.length, 1); + assert.strictEqual((result[0] as typeof items[0]).name, 'A'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/customizationCreatorService.test.ts b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/customizationCreatorService.test.ts index 5e27539c76710..0b5d81754fb01 100644 --- a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/customizationCreatorService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/customizationCreatorService.test.ts @@ -9,37 +9,10 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/ import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; import { IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; import { resolveUserTargetDirectory } from '../../../browser/aiCustomization/customizationCreatorService.js'; -import { ChatConfiguration } from '../../../common/constants.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { IPathService } from '../../../../../services/path/common/pathService.js'; suite('customizationCreatorService', () => { ensureNoDisposablesAreLeakedInTestSuite(); - const userHome = URI.file('/home/user'); - - function createMockPathService(): Pick { - return { - userHome: ((options?: { preferLocal: boolean }) => { - if (options?.preferLocal) { - return userHome; - } - return Promise.resolve(userHome); - }) as IPathService['userHome'], - }; - } - - function createMockConfigService(overridePath: string): Pick { - return { - getValue: (key: string) => { - if (key === ChatConfiguration.ChatCustomizationUserStoragePath) { - return overridePath; - } - return undefined; - }, - } as Pick; - } - function createMockPromptsService(userFolderUri?: URI): Pick { return { getSourceFolders: () => Promise.resolve( @@ -52,89 +25,21 @@ suite('customizationCreatorService', () => { suite('resolveUserTargetDirectory', () => { - test('with override path and tilde for instructions', async () => { + test('returns user folder from getSourceFolders', async () => { + const userFolder = URI.file('/home/user/.copilot/instructions'); const result = await resolveUserTargetDirectory( - createMockPromptsService() as IPromptsService, + createMockPromptsService(userFolder) as IPromptsService, PromptsType.instructions, - createMockConfigService('~/.copilot') as IConfigurationService, - createMockPathService() as IPathService, ); assert.strictEqual(result?.path, '/home/user/.copilot/instructions'); }); - test('with override path and tilde for skills', async () => { - const result = await resolveUserTargetDirectory( - createMockPromptsService() as IPromptsService, - PromptsType.skill, - createMockConfigService('~/.copilot') as IConfigurationService, - createMockPathService() as IPathService, - ); - assert.strictEqual(result?.path, '/home/user/.copilot/skills'); - }); - - test('override path is ignored for prompts (no CLI discovery path)', async () => { - const fallbackUri = URI.file('/home/user/.vscode/prompts'); - const result = await resolveUserTargetDirectory( - createMockPromptsService(fallbackUri) as IPromptsService, - PromptsType.prompt, - createMockConfigService('~/.copilot') as IConfigurationService, - createMockPathService() as IPathService, - ); - // Should fall through to getSourceFolders, not use the override - assert.strictEqual(result?.path, '/home/user/.vscode/prompts'); - }); - - test('override path is ignored for agents (no CLI convention)', async () => { - const fallbackUri = URI.file('/home/user/.vscode/prompts'); - const result = await resolveUserTargetDirectory( - createMockPromptsService(fallbackUri) as IPromptsService, - PromptsType.agent, - createMockConfigService('~/.copilot') as IConfigurationService, - createMockPathService() as IPathService, - ); - // Should fall through to getSourceFolders, not use the override - assert.strictEqual(result?.path, '/home/user/.vscode/prompts'); - }); - - test('override path is ignored for hooks (no CLI convention)', async () => { + test('returns undefined when no user folder exists', async () => { const result = await resolveUserTargetDirectory( createMockPromptsService() as IPromptsService, PromptsType.hook, - createMockConfigService('~/.copilot') as IConfigurationService, - createMockPathService() as IPathService, ); - // No user folder for hooks, should return undefined assert.strictEqual(result, undefined); }); - - test('falls back to getSourceFolders when no override is set', async () => { - const fallbackUri = URI.file('/home/user/.vscode/prompts'); - const result = await resolveUserTargetDirectory( - createMockPromptsService(fallbackUri) as IPromptsService, - PromptsType.instructions, - createMockConfigService('') as IConfigurationService, - createMockPathService() as IPathService, - ); - assert.strictEqual(result?.path, '/home/user/.vscode/prompts'); - }); - - test('falls back to getSourceFolders when no config service provided', async () => { - const fallbackUri = URI.file('/home/user/.vscode/prompts'); - const result = await resolveUserTargetDirectory( - createMockPromptsService(fallbackUri) as IPromptsService, - PromptsType.instructions, - ); - assert.strictEqual(result?.path, '/home/user/.vscode/prompts'); - }); - - test('with absolute override path (no tilde)', async () => { - const result = await resolveUserTargetDirectory( - createMockPromptsService() as IPromptsService, - PromptsType.instructions, - createMockConfigService('/custom/path') as IConfigurationService, - createMockPathService() as IPathService, - ); - assert.strictEqual(result?.path, '/custom/path/instructions'); - }); }); }); From 6c39741e80c4776b9ba4c1c12c3bfc639ca28729 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 27 Feb 2026 01:23:05 +0000 Subject: [PATCH 43/50] Telemetry tweak (#298116) --- .../workbench/contrib/chat/common/chatService/chatService.ts | 4 ++++ .../contrib/chat/common/chatService/chatServiceImpl.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index c2edad1d45c52..3d31f408577b1 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1489,6 +1489,8 @@ export type ChatStopCancellationNoopEvent = { reason: 'noWidget' | 'noViewModel' | 'noPendingRequest' | 'requestAlreadyCanceled' | 'requestIdUnavailable'; requestInProgress: 'true' | 'false' | 'unknown'; pendingRequests: number; + sessionScheme?: string; + lastRequestId?: string; }; export type ChatStopCancellationNoopClassification = { @@ -1496,6 +1498,8 @@ export type ChatStopCancellationNoopClassification = { reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The no-op reason when stop cancellation did not dispatch fully.' }; requestInProgress: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether request-in-progress was true, false, or unknown at no-op time.' }; pendingRequests: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of queued pending requests at no-op time when known.'; isMeasurement: true }; + sessionScheme?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The URI scheme of the session resource (e.g. vscodeLocalChatSession vs remote).' }; + lastRequestId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the last request in the session, for correlating with tool invocations.' }; owner: 'roblourens'; comment: 'Tracks possible no-op stop cancellation paths.'; }; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 57a993bb914d0..6b2d3a5aefcd4 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1441,11 +1441,14 @@ export class ChatService extends Disposable implements IChatService { const model = this._sessionModels.get(sessionResource); const requestInProgress = model?.requestInProgress.get(); const pendingRequestsCount = model?.getPendingRequests().length ?? 0; + const lastRequest = model?.lastRequest; this.telemetryService.publicLog2(ChatStopCancellationNoopEventName, { source: source ?? 'chatService', reason: 'noPendingRequest', requestInProgress: requestInProgress === undefined ? 'unknown' : requestInProgress ? 'true' : 'false', pendingRequests: pendingRequestsCount, + sessionScheme: sessionResource.scheme, + lastRequestId: lastRequest?.id, }); this.info('cancelCurrentRequestForSession', `No pending request was found for session ${sessionResource}. requestInProgress=${requestInProgress ?? 'unknown'}, pendingRequests=${pendingRequestsCount}`); return; From 3c0e1a1b1d7516936c35d979bb299cb0f66f99ac Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 27 Feb 2026 02:23:07 +0100 Subject: [PATCH 44/50] bring back adding workspace folder with files view (#298130) * bring back adding workspace folder with files view * feedback * fix compilation * add collapse all action --- .../browser/changesView.contribution.ts | 4 + .../browser/toggleChangesView.ts} | 19 +++-- .../browser/configuration.contribution.ts | 8 +- .../files/browser/files.contribution.ts | 83 +++++++++++++++++++ .../contrib/logs/browser/logs.contribution.ts | 7 +- .../sessions/browser/sessions.contribution.ts | 2 - .../browser/sessionsManagementService.ts | 6 +- .../browser/workspace.contribution.ts | 9 ++ .../browser/workspaceFolderManagement.ts | 80 ++++++++++++++++++ .../browser/workspaceContextService.ts | 16 +++- src/vs/sessions/sessions.desktop.main.ts | 2 + 11 files changed, 215 insertions(+), 21 deletions(-) rename src/vs/sessions/contrib/{sessions/browser/sessionsAuxiliaryBarContribution.ts => changesView/browser/toggleChangesView.ts} (89%) create mode 100644 src/vs/sessions/contrib/files/browser/files.contribution.ts create mode 100644 src/vs/sessions/contrib/workspace/browser/workspace.contribution.ts create mode 100644 src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.contribution.ts b/src/vs/sessions/contrib/changesView/browser/changesView.contribution.ts index f5ee5daff6440..9da044d818d4b 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.contribution.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.contribution.ts @@ -8,9 +8,11 @@ import { localize2 } from '../../../../nls.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { IViewContainersRegistry, ViewContainerLocation, IViewsRegistry, Extensions as ViewContainerExtensions, WindowVisibility } from '../../../../workbench/common/views.js'; import { CHANGES_VIEW_CONTAINER_ID, CHANGES_VIEW_ID, ChangesViewPane, ChangesViewPaneContainer } from './changesView.js'; import './changesViewActions.js'; +import { ToggleChangesViewContribution } from './toggleChangesView.js'; const changesViewIcon = registerIcon('changes-view-icon', Codicon.gitCompare, localize2('changesViewIcon', 'View icon for the Changes view.').value); @@ -39,3 +41,5 @@ viewsRegistry.registerViews([{ order: 1, windowVisibility: WindowVisibility.Sessions }], changesViewContainer); + +registerWorkbenchContribution2(ToggleChangesViewContribution.ID, ToggleChangesViewContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsAuxiliaryBarContribution.ts b/src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts similarity index 89% rename from src/vs/sessions/contrib/sessions/browser/sessionsAuxiliaryBarContribution.ts rename to src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts index eb36b915ad62d..e7371a4f73d22 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsAuxiliaryBarContribution.ts +++ b/src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts @@ -14,16 +14,18 @@ import { IChatService } from '../../../../workbench/contrib/chat/common/chatServ import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; -import { ISessionsManagementService } from './sessionsManagementService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; +import { CHANGES_VIEW_ID } from './changesView.js'; interface IPendingTurnState { readonly hadChangesBeforeSend: boolean; readonly submittedAt: number; } -export class SessionsAuxiliaryBarContribution extends Disposable { +export class ToggleChangesViewContribution extends Disposable { - static readonly ID = 'workbench.contrib.sessionsAuxiliaryBarContribution'; + static readonly ID = 'workbench.contrib.toggleChangesView'; private readonly pendingTurnStateByResource = new ResourceMap(); @@ -33,6 +35,7 @@ export class SessionsAuxiliaryBarContribution extends Disposable { @IChatEditingService private readonly chatEditingService: IChatEditingService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IChatService private readonly chatService: IChatService, + @IViewsService private readonly viewsService: IViewsService, ) { super(); @@ -106,12 +109,10 @@ export class SessionsAuxiliaryBarContribution extends Disposable { } private syncAuxiliaryBarVisibility(hasChanges: boolean): void { - const shouldHideAuxiliaryBar = !hasChanges; - const isAuxiliaryBarVisible = this.layoutService.isVisible(Parts.AUXILIARYBAR_PART); - if (shouldHideAuxiliaryBar === !isAuxiliaryBarVisible) { - return; + if (hasChanges) { + this.viewsService.openView(CHANGES_VIEW_ID, true); + } else { + this.layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); } - - this.layoutService.setPartHidden(shouldHideAuxiliaryBar, Parts.AUXILIARYBAR_PART); } } diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 718a5ad73e28a..467955a64bdc1 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -10,6 +10,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon overrides: { 'chat.agentsControl.enabled': true, 'chat.agent.maxRequests': 1000, + 'chat.customizationsMenu.userStoragePath': '~/.copilot', 'chat.restoreLastPanelSession': true, 'chat.unifiedAgentsBar.enabled': true, 'chat.viewSessions.enabled': false, @@ -19,6 +20,8 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'diffEditor.renderSideBySide': false, 'diffEditor.hideUnchangedRegions.enabled': true, + 'extensions.ignoreRecommendations': true, + 'files.autoSave': 'afterDelay', 'git.autofetch': true, @@ -33,6 +36,8 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'inlineChat.affordance': 'editor', 'inlineChat.renderMode': 'hover', + 'terminal.integrated.initialHint': false, + 'workbench.editor.restoreEditors': false, 'workbench.editor.showTabs': 'single', 'workbench.startupEditor': 'none', @@ -40,10 +45,9 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'workbench.layoutControl.type': 'toggles', 'workbench.editor.useModal': 'all', 'workbench.panel.showLabels': false, + 'window.menuStyle': 'custom', 'window.dialogStyle': 'custom', - - 'terminal.integrated.initialHint': false }, donotCache: true, preventExperimentOverride: true, diff --git a/src/vs/sessions/contrib/files/browser/files.contribution.ts b/src/vs/sessions/contrib/files/browser/files.contribution.ts new file mode 100644 index 0000000000000..6d9f6fa696a4e --- /dev/null +++ b/src/vs/sessions/contrib/files/browser/files.contribution.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { localize2 } from '../../../../nls.js'; +import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { IViewContainersRegistry, IViewsRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, WindowVisibility } from '../../../../workbench/common/views.js'; +import { WorkspaceFolderCountContext } from '../../../../workbench/common/contextkeys.js'; +import { ExplorerView } from '../../../../workbench/contrib/files/browser/views/explorerView.js'; +import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js'; +import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; + +const SESSIONS_FILES_CONTAINER_ID = 'workbench.sessions.auxiliaryBar.filesContainer'; +const SESSIONS_FILES_VIEW_ID = 'sessions.files.explorer'; + +const filesViewIcon = registerIcon('sessions-files-view-icon', Codicon.files, localize2('sessionsFilesViewIcon', 'View icon of the files view in the sessions window.').value); + +class RegisterFilesViewContribution implements IWorkbenchContribution { + + static readonly ID = 'sessions.registerFilesView'; + + constructor() { + const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); + const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + + // Register a new Files view container in the auxiliary bar for the sessions window + const filesViewContainer = viewContainerRegistry.registerViewContainer({ + id: SESSIONS_FILES_CONTAINER_ID, + title: localize2('files', "Files"), + icon: filesViewIcon, + order: 11, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [SESSIONS_FILES_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]), + storageId: SESSIONS_FILES_CONTAINER_ID, + hideIfEmpty: true, + windowVisibility: WindowVisibility.Sessions, + }, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); + + // Re-register the explorer view inside the new Files container + viewsRegistry.registerViews([{ + id: SESSIONS_FILES_VIEW_ID, + name: localize2('files', "Files"), + containerIcon: filesViewIcon, + ctorDescriptor: new SyncDescriptor(ExplorerView), + canToggleVisibility: true, + canMoveView: false, + when: WorkspaceFolderCountContext.notEqualsTo('0'), + windowVisibility: WindowVisibility.Sessions, + }], filesViewContainer); + } +} + +registerWorkbenchContribution2(RegisterFilesViewContribution.ID, RegisterFilesViewContribution, WorkbenchPhase.AfterRestored); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'sessions.files.action.collapseExplorerFolders', + title: localize2('collapseExplorerFolders', "Collapse Folders in Explorer"), + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyExpr.equals('view', SESSIONS_FILES_VIEW_ID), + }, + }); + } + + run(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const view = viewsService.getViewWithId(SESSIONS_FILES_VIEW_ID); + if (view !== null) { + (view as ExplorerView).collapseAll(); + } + } +}); diff --git a/src/vs/sessions/contrib/logs/browser/logs.contribution.ts b/src/vs/sessions/contrib/logs/browser/logs.contribution.ts index e6de07b259404..b401f4e70ee4f 100644 --- a/src/vs/sessions/contrib/logs/browser/logs.contribution.ts +++ b/src/vs/sessions/contrib/logs/browser/logs.contribution.ts @@ -18,6 +18,7 @@ import { IViewContainersRegistry, IViewsRegistry, ViewContainerLocation, Extensi import { OutputViewPane } from '../../../../workbench/contrib/output/browser/outputView.js'; import { OUTPUT_VIEW_ID } from '../../../../workbench/services/output/common/output.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; +import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; const SESSIONS_LOGS_CONTAINER_ID = 'workbench.sessions.panel.logsContainer'; @@ -29,7 +30,11 @@ class RegisterLogsViewContainerContribution implements IWorkbenchContribution { static readonly ID = 'sessions.registerLogsViewContainer'; - constructor() { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @IEnvironmentService environmentService: IEnvironmentService, + ) { + CONTEXT_SESSIONS_SHOW_LOGS.bindTo(contextKeyService).set(!environmentService.isBuilt); const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); diff --git a/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts b/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts index b81e8fa62848f..52bd441711531 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts @@ -13,7 +13,6 @@ import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/vie import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { SessionsTitleBarContribution } from './sessionsTitleBarWidget.js'; -import { SessionsAuxiliaryBarContribution } from './sessionsAuxiliaryBarContribution.js'; import { AgenticSessionsViewPane, SessionsViewId } from './sessionsViewPane.js'; import { SessionsManagementService, ISessionsManagementService } from './sessionsManagementService.js'; @@ -47,6 +46,5 @@ const agentSessionsViewDescriptor: IViewDescriptor = { Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([agentSessionsViewDescriptor], agentSessionsViewContainer); registerWorkbenchContribution2(SessionsTitleBarContribution.ID, SessionsTitleBarContribution, WorkbenchPhase.AfterRestored); -registerWorkbenchContribution2(SessionsAuxiliaryBarContribution.ID, SessionsAuxiliaryBarContribution, WorkbenchPhase.AfterRestored); registerSingleton(ISessionsManagementService, SessionsManagementService, InstantiationType.Delayed); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 8524549cea332..eb6848e9af8ae 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -99,7 +99,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa private readonly _activeSession = observableValue(this, undefined); readonly activeSession: IObservable = this._activeSession; - private readonly _activeSessionDisposables = this._register(new DisposableStore()); + private readonly _newActiveSessionDisposables = this._register(new DisposableStore()); private readonly _newSession = this._register(new MutableDisposable()); private lastSelectedSession: URI | undefined; @@ -400,7 +400,6 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } private setActiveSession(session: IAgentSession | INewSession | undefined): void { - this._activeSessionDisposables.clear(); let activeSessionItem: IActiveSessionItem | undefined; if (session) { if (isAgentSession(session)) { @@ -423,7 +422,8 @@ export class SessionsManagementService extends Disposable implements ISessionsMa worktree: undefined, providerType: session.target, }; - this._activeSessionDisposables.add(session.onDidChange(e => { + this._newActiveSessionDisposables.clear(); + this._newActiveSessionDisposables.add(session.onDidChange(e => { if (e === 'repoUri') { this.doSetActiveSession({ isUntitled: true, diff --git a/src/vs/sessions/contrib/workspace/browser/workspace.contribution.ts b/src/vs/sessions/contrib/workspace/browser/workspace.contribution.ts new file mode 100644 index 0000000000000..4351598efa6b2 --- /dev/null +++ b/src/vs/sessions/contrib/workspace/browser/workspace.contribution.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { WorkspaceFolderManagementContribution } from './workspaceFolderManagement.js'; + +registerWorkbenchContribution2(WorkspaceFolderManagementContribution.ID, WorkspaceFolderManagementContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts new file mode 100644 index 0000000000000..44fa452df3abe --- /dev/null +++ b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; +import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { URI } from '../../../../base/common/uri.js'; +import { autorun } from '../../../../base/common/observable.js'; + +export class WorkspaceFolderManagementContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.workspaceFolderManagement'; + + constructor( + @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + ) { + super(); + this._register(autorun(reader => { + const activeSession = this.sessionManagementService.activeSession.read(reader); + this.updateWorkspaceFoldersForSession(activeSession); + })); + } + + private async updateWorkspaceFoldersForSession(session: IActiveSessionItem | undefined): Promise { + await this.manageTrustWorkspaceForSession(session); + const activeSessionRepo = session?.providerType === AgentSessionProviders.Background ? session.worktree ?? session.repository : undefined; + const currentRepo = this.workspaceContextService.getWorkspace().folders[0]?.uri; + + if (!activeSessionRepo) { + if (currentRepo) { + await this.workspaceEditingService.removeFolders([currentRepo], true); + } + return; + } + + if (!currentRepo) { + await this.workspaceEditingService.addFolders([{ uri: activeSessionRepo }], true); + return; + } + + if (this.uriIdentityService.extUri.isEqual(currentRepo, activeSessionRepo)) { + return; + } + + await this.workspaceEditingService.updateFolders(0, 1, [{ uri: activeSessionRepo }], true); + } + + private async manageTrustWorkspaceForSession(session: IActiveSessionItem | undefined): Promise { + if (session?.providerType !== AgentSessionProviders.Background) { + return; + } + + if (!session.repository || !session.worktree) { + return; + } + + if (!this.isUriTrusted(session.repository)) { + return; + } + + if (!this.isUriTrusted(session.worktree)) { + await this.workspaceTrustManagementService.setUrisTrust([session.worktree], true); + } + } + + private isUriTrusted(uri: URI): boolean { + return this.workspaceTrustManagementService.getTrustedUris().some(trustedUri => this.uriIdentityService.extUri.isEqual(trustedUri, uri)); + } +} diff --git a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts index 660d475ee8b94..b9ec9f571f9c5 100644 --- a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts +++ b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../../base/common/event.js'; +import { Queue } from '../../../../base/common/async.js'; import { removeTrailingPathSeparator } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -11,8 +12,9 @@ import { Workspace, WorkspaceFolder, IWorkspace, IWorkspaceContextService, IWork import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/common/workspaces.js'; import { getWorkspaceIdentifier } from '../../../../workbench/services/workspaces/browser/workspaces.js'; import { IDidEnterWorkspaceEvent, IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; -export class SessionsWorkspaceContextService implements IWorkspaceContextService, IWorkspaceEditingService { +export class SessionsWorkspaceContextService extends Disposable implements IWorkspaceContextService, IWorkspaceEditingService { declare readonly _serviceBrand: undefined; @@ -23,15 +25,17 @@ export class SessionsWorkspaceContextService implements IWorkspaceContextService private readonly _onWillChangeWorkspaceFolders = new Emitter(); readonly onWillChangeWorkspaceFolders = this._onWillChangeWorkspaceFolders.event; - private readonly _onDidChangeWorkspaceFolders = new Emitter(); + private readonly _onDidChangeWorkspaceFolders = this._register(new Emitter()); readonly onDidChangeWorkspaceFolders = this._onDidChangeWorkspaceFolders.event; private workspace: Workspace; + private readonly _updateFoldersQueue = this._register(new Queue()); constructor( sessionsWorkspaceUri: URI, private readonly uriIdentityService: IUriIdentityService ) { + super(); const workspaceIdentifier = getWorkspaceIdentifier(sessionsWorkspaceUri); this.workspace = new Workspace(workspaceIdentifier.id, [], false, workspaceIdentifier.configPath, uri => uriIdentityService.extUri.ignorePathCasing(uri)); } @@ -45,7 +49,7 @@ export class SessionsWorkspaceContextService implements IWorkspaceContextService } getWorkbenchState(): WorkbenchState { - return WorkbenchState.WORKSPACE; + return WorkbenchState.EMPTY; } getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { @@ -94,7 +98,11 @@ export class SessionsWorkspaceContextService implements IWorkspaceContextService async pickNewWorkspacePath(): Promise { return undefined; } - private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise { + private doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise { + return this._updateFoldersQueue.queue(() => this._doUpdateFolders(foldersToAdd, foldersToRemove, index)); + } + + private async _doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise { if (foldersToAdd.length === 0 && foldersToRemove.length === 0) { return; } diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 7089131ba679c..406ea03e50834 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -206,6 +206,7 @@ import './contrib/chat/browser/chat.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; import './contrib/changesView/browser/changesView.contribution.js'; +import './contrib/files/browser/files.contribution.js'; import './contrib/gitSync/browser/gitSync.contribution.js'; import './contrib/applyToParentRepo/browser/applyToParentRepo.contribution.js'; import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed @@ -213,6 +214,7 @@ import './contrib/configuration/browser/configuration.contribution.js'; import './contrib/terminal/browser/sessionsTerminalContribution.js'; import './contrib/logs/browser/logs.contribution.js'; +import './contrib/workspace/browser/workspace.contribution.js'; import './contrib/welcome/browser/welcome.contribution.js'; //#endregion From 0f50837723387fd14963676c1c4e2dfc8a673bd1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 27 Feb 2026 02:23:26 +0100 Subject: [PATCH 45/50] enhane attaching files and folders (#298133) * enhane attaching files and folders * feedback --- .../contrib/chat/browser/media/chatWidget.css | 35 ++++- .../chat/browser/newChatContextAttachments.ts | 148 +++++++++++------- .../contrib/chat/browser/newChatViewPane.ts | 2 +- 3 files changed, 121 insertions(+), 64 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css index b23633e013fdb..247b4cdae062a 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css @@ -8,6 +8,7 @@ flex-direction: column; height: 100%; width: 100%; + position: relative; } /* Welcome container fills available space and centers content */ @@ -250,17 +251,37 @@ } /* Drag and drop */ -.sessions-chat-drop-overlay { - display: none; +.sessions-chat-dnd-overlay { position: absolute; top: 0; left: 0; - right: 0; - bottom: 0; + width: 100%; + height: 100%; + box-sizing: border-box; + display: none; z-index: 10; + background-color: var(--vscode-sideBar-dropBackground, var(--vscode-list-dropBackground)); +} + +.sessions-chat-dnd-overlay.visible { + display: flex; + align-items: center; + justify-content: center; } -.sessions-chat-input-area.sessions-chat-drop-active { - border-color: var(--vscode-focusBorder); - background-color: var(--vscode-list-dropBackground); +.sessions-chat-dnd-overlay .attach-context-overlay-text { + padding: 0.6em; + margin: 0.2em; + line-height: 12px; + height: 12px; + display: flex; + align-items: center; + text-align: center; + background-color: var(--vscode-sideBar-background, var(--vscode-editor-background)); +} + +.sessions-chat-dnd-overlay .attach-context-overlay-text .codicon { + height: 12px; + font-size: 12px; + margin-right: 3px; } diff --git a/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts b/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts index 09b900d5a7285..a7440dfc69112 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../base/browser/dom.js'; +import { DragAndDropObserver } from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter } from '../../../../base/common/event.js'; -import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { renderIcon, renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { localize } from '../../../../nls.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { registerOpenEditorListeners } from '../../../../platform/editor/browser/editor.js'; @@ -29,7 +30,8 @@ import { IChatRequestVariableEntry, OmittedState } from '../../../../workbench/c import { isLocation } from '../../../../editor/common/languages.js'; import { resizeImage } from '../../../../workbench/contrib/chat/browser/chatImageUtils.js'; import { imageToHash, isImage } from '../../../../workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.js'; -import { getPathForFile } from '../../../../platform/dnd/browser/dnd.js'; +import { CodeDataTransfers, containsDragType, extractEditorsDropData, getPathForFile } from '../../../../platform/dnd/browser/dnd.js'; +import { DataTransfers } from '../../../../base/browser/dnd.js'; import { getExcludes, ISearchConfiguration, ISearchService, QueryType } from '../../../../workbench/services/search/common/search.js'; /** @@ -101,7 +103,7 @@ export class NewChatContextAttachments extends Disposable { const pill = dom.append(this._container, dom.$('.sessions-chat-attachment-pill')); pill.tabIndex = 0; pill.role = 'button'; - const icon = entry.kind === 'image' ? Codicon.fileMedia : Codicon.file; + const icon = entry.kind === 'image' ? Codicon.fileMedia : entry.kind === 'directory' ? Codicon.folder : Codicon.file; dom.append(pill, renderIcon(icon)); dom.append(pill, dom.$('span.sessions-chat-attachment-name', undefined, entry.name)); @@ -127,68 +129,85 @@ export class NewChatContextAttachments extends Disposable { // --- Drag and drop --- - registerDropTarget(element: HTMLElement): void { - // Use a transparent overlay during drag to capture events over the Monaco editor - const overlay = dom.append(element, dom.$('.sessions-chat-drop-overlay')); + registerDropTarget(dndContainer: HTMLElement): void { + const overlay = dom.append(dndContainer, dom.$('.sessions-chat-dnd-overlay')); + let overlayText: HTMLElement | undefined; - // Use capture phase to intercept drag events before Monaco editor handles them - this._register(dom.addDisposableListener(element, dom.EventType.DRAG_ENTER, (e: DragEvent) => { - if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) { - e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; - overlay.style.display = 'block'; - element.classList.add('sessions-chat-drop-active'); - } - }, true)); + const isDropSupported = (e: DragEvent): boolean => { + return containsDragType(e, DataTransfers.FILES, CodeDataTransfers.EDITORS, CodeDataTransfers.FILES, DataTransfers.RESOURCES, DataTransfers.INTERNAL_URI_LIST); + }; - this._register(dom.addDisposableListener(element, dom.EventType.DRAG_OVER, (e: DragEvent) => { - if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) { - e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; - if (overlay.style.display !== 'block') { - overlay.style.display = 'block'; - element.classList.add('sessions-chat-drop-active'); - } + const showOverlay = () => { + overlay.classList.add('visible'); + if (!overlayText) { + const label = localize('attachAsContext', "Attach as Context"); + const iconAndTextElements = renderLabelWithIcons(`$(${Codicon.attach.id}) ${label}`); + const htmlElements = iconAndTextElements.map(element => { + if (typeof element === 'string') { + return dom.$('span.overlay-text', undefined, element); + } + return element; + }); + overlayText = dom.$('span.attach-context-overlay-text', undefined, ...htmlElements); + overlay.appendChild(overlayText); } - }, true)); - - this._register(dom.addDisposableListener(overlay, dom.EventType.DRAG_OVER, (e) => { - e.preventDefault(); - e.dataTransfer!.dropEffect = 'copy'; - })); + }; - this._register(dom.addDisposableListener(overlay, dom.EventType.DRAG_LEAVE, (e) => { - if (e.relatedTarget && element.contains(e.relatedTarget as Node)) { - return; - } - overlay.style.display = 'none'; - element.classList.remove('sessions-chat-drop-active'); - })); + const hideOverlay = () => { + overlay.classList.remove('visible'); + overlayText?.remove(); + overlayText = undefined; + }; - this._register(dom.addDisposableListener(overlay, dom.EventType.DROP, async (e) => { - e.preventDefault(); - e.stopPropagation(); - overlay.style.display = 'none'; - element.classList.remove('sessions-chat-drop-active'); - - // Try items first (for URI-based drops from VS Code tree views) - const items = e.dataTransfer?.items; - if (items) { - for (const item of Array.from(items)) { - if (item.kind === 'file') { - const file = item.getAsFile(); - if (!file) { - continue; + this._register(new DragAndDropObserver(dndContainer, { + onDragOver: (e) => { + if (isDropSupported(e)) { + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy'; + } + showOverlay(); + } + }, + onDragLeave: () => { + hideOverlay(); + }, + onDrop: async (e) => { + e.preventDefault(); + e.stopPropagation(); + hideOverlay(); + + // Extract editor data from VS Code internal drags (e.g., explorer view) + const editorDropData = extractEditorsDropData(e); + if (editorDropData.length > 0) { + for (const editor of editorDropData) { + if (editor.resource) { + await this._attachFileUri(editor.resource, basename(editor.resource)); } - const filePath = getPathForFile(file); - if (!filePath) { - continue; + } + return; + } + + // Fallback: try native file items + const items = e.dataTransfer?.items; + if (items) { + for (const item of Array.from(items)) { + if (item.kind === 'file') { + const file = item.getAsFile(); + if (!file) { + continue; + } + const filePath = getPathForFile(file); + if (!filePath) { + continue; + } + const uri = URI.file(filePath); + await this._attachFileUri(uri, file.name); } - const uri = URI.file(filePath); - await this._attachFileUri(uri, file.name); } } - } + }, })); } @@ -446,6 +465,23 @@ export class NewChatContextAttachments extends Disposable { } private async _attachFileUri(uri: URI, name: string): Promise { + let stat; + try { + stat = await this.fileService.stat(uri); + } catch { + return; + } + + if (stat.isDirectory) { + this._addAttachments({ + kind: 'directory', + id: uri.toString(), + value: uri, + name, + }); + return; + } + if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(uri.path)) { const readFile = await this.fileService.readFile(uri); const resizedImage = await resizeImage(readFile.value.buffer); diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 0395a03ae2f0b..a0d367ea964d6 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -241,7 +241,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // Input area inside the input slot const inputArea = dom.$('.sessions-chat-input-area'); - this._contextAttachments.registerDropTarget(inputArea); + this._contextAttachments.registerDropTarget(wrapper); this._contextAttachments.registerPasteHandler(inputArea); // Attachments row (pills only) inside input area, above editor From 76080f7bcdb717a518df501dbd80668aa9c53be9 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 26 Feb 2026 17:34:29 -0800 Subject: [PATCH 46/50] enhance new chat button functionality with additional icon variants and context key updates (#298136) * enhance new chat button functionality with additional icon variants and context key updates * Update src/vs/workbench/contrib/chat/browser/chat.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../chat/browser/actions/chatActions.ts | 34 +++++++++++++--- .../chat/browser/actions/chatNewActions.ts | 39 +++++++++++++++++++ .../contrib/chat/browser/chat.contribution.ts | 3 +- .../chat/common/actions/chatContextKeys.ts | 2 +- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index f2cfdd54ec47d..d1232f91828ff 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -679,7 +679,7 @@ export function registerChatActions() { }, { id: MenuId.EditorTitle, group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('copilot'), ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('sparkle')), + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('copilot'), ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('new-session'), ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('comment')), order: 1 }], }); @@ -715,19 +715,43 @@ export function registerChatActions() { } }); - registerAction2(class NewChatEditorSparkleIconAction extends Action2 { + registerAction2(class NewChatEditorNewSessionIconAction extends Action2 { constructor() { super({ - id: ACTION_ID_OPEN_CHAT + '.sparkleIcon', + id: ACTION_ID_OPEN_CHAT + '.newSessionIcon', title: localize2('interactiveSession.open', "New Chat Editor"), - icon: Codicon.chatSparkle, + icon: Codicon.newSession, f1: false, category: CHAT_CATEGORY, precondition: ChatContextKeys.enabled, menu: [{ id: MenuId.EditorTitle, group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ChatContextKeys.newChatButtonExperimentIcon.isEqualTo('sparkle')), + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ChatContextKeys.newChatButtonExperimentIcon.isEqualTo('new-session')), + order: 1 + }], + }); + } + + async run(accessor: ServicesAccessor) { + const widgetService = accessor.get(IChatWidgetService); + await widgetService.openSession(LocalChatSessionUri.getNewSessionUri(), ACTIVE_GROUP, { pinned: true } satisfies IChatEditorOptions); + } + }); + + registerAction2(class NewChatEditorCommentIconAction extends Action2 { + constructor() { + super({ + id: ACTION_ID_OPEN_CHAT + '.commentIcon', + title: localize2('interactiveSession.open', "New Chat Editor"), + icon: Codicon.comment, + f1: false, + category: CHAT_CATEGORY, + precondition: ChatContextKeys.enabled, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ChatContextKeys.newChatButtonExperimentIcon.isEqualTo('comment')), order: 1 }], }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index febdbc539f7fb..271e82b2baf41 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -108,6 +108,11 @@ export function registerNewChatActions() { id: MenuId.ChatNewMenu, group: '1_open', order: 1, + when: ContextKeyExpr.and( + ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('copilot'), + ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('new-session'), + ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('comment') + ) } ], keybinding: { @@ -132,6 +137,40 @@ export function registerNewChatActions() { } } ); + + const iconVariants = [ + { idSuffix: '.copilotIcon', iconValue: 'copilot', icon: Codicon.copilot }, + { idSuffix: '.newSessionIcon', iconValue: 'new-session', icon: Codicon.newSession }, + { idSuffix: '.commentIcon', iconValue: 'comment', icon: Codicon.comment }, + ] as const; + + for (const variant of iconVariants) { + registerAction2(class extends Action2 { + constructor() { + super({ + id: ACTION_ID_NEW_CHAT + variant.idSuffix, + title: localize2('chat.newEdits.label', "New Chat"), + category: CHAT_CATEGORY, + icon: variant.icon, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)), + f1: false, + menu: [{ + id: MenuId.ChatNewMenu, + group: '1_open', + order: 1, + when: ChatContextKeys.newChatButtonExperimentIcon.isEqualTo(variant.iconValue) + }] + }); + } + + async run(accessor: ServicesAccessor, ...args: unknown[]) { + const executeCommandContext = isNewEditSessionActionContext(args[0]) ? args[0] : undefined; + const context = getEditingSessionContext(accessor, args); + await runNewChatAction(accessor, context, executeCommandContext); + } + }); + } + CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_EDIT_SESSION, ACTION_ID_NEW_CHAT); registerAction2(class NewLocalChatAction extends Action2 { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 3d8d31df97f53..d50fc45f037c1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1457,7 +1457,8 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr private registerNewChatButtonIcon(): void { this.experimentService.getTreatment('chatNewButtonIcon').then((value) => { - if (value === 'copilot' || value === 'sparkle') { + const supportedValues = ['copilot', 'new-session', 'comment']; + if (typeof value === 'string' && supportedValues.includes(value)) { this.newChatButtonExperimentIcon.set(value); } else { this.newChatButtonExperimentIcon.reset(); diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index d7e4763aa4d76..8e703e221f7b6 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -141,7 +141,7 @@ export namespace ChatContextKeys { export const contextUsageHasBeenOpened = new RawContextKey('chatContextUsageHasBeenOpened', false, { type: 'boolean', description: localize('chatContextUsageHasBeenOpened', "True when the user has opened the context window usage details.") }); - export const newChatButtonExperimentIcon = new RawContextKey('chatNewChatButtonExperimentIcon', '', { type: 'string', description: localize('chatNewChatButtonExperimentIcon', "The icon variant for the new chat button, controlled by experiment. Values: 'copilot', 'sparkle', or empty for default.") }); + export const newChatButtonExperimentIcon = new RawContextKey('chatNewChatButtonExperimentIcon', '', { type: 'string', description: localize('chatNewChatButtonExperimentIcon', "The icon variant for the new chat button, controlled by experiment. Values: 'copilot', 'new-session', 'comment', or empty for default.") }); } export namespace ChatContextKeyExprs { From 9ba3e6af044f3425a4462567e90d76c858e8ebc4 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Thu, 26 Feb 2026 17:35:36 -0800 Subject: [PATCH 47/50] Revert "Bump hono from 4.12.0 to 4.12.3 in /test/mcp" (#298137) Revert "Bump hono from 4.12.0 to 4.12.3 in /test/mcp (#298076)" This reverts commit 4ce5eb1a83cd7f4cff92d27a9a79cd290694d3ff. --- test/mcp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 6e7624dc0da22..f3a0ceb0bfffd 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -702,9 +702,9 @@ } }, "node_modules/hono": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", - "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz", + "integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==", "license": "MIT", "engines": { "node": ">=16.9.0" From a042a1cef0ab2bcc015143f36a294784170d6588 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:00:11 -0800 Subject: [PATCH 48/50] Reorder keyboard hints in empty editor window (#298140) * Initial plan * reorder keyboard hints in empty editor window: move Open Recent before Open File or Folder Co-authored-by: jo-oikawa <14115185+jo-oikawa@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jo-oikawa <14115185+jo-oikawa@users.noreply.github.com> --- src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index a5b6b341f20ac..03bfed510efa8 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -48,8 +48,8 @@ const baseEntries: WatermarkEntry[] = [ const emptyWindowEntries: WatermarkEntry[] = coalesce([ ...baseEntries, - ...(isMacintosh && !isWeb ? [openFileOrFolder] : [openFile, openFolder]), openRecent, + ...(isMacintosh && !isWeb ? [openFileOrFolder] : [openFile, openFolder]), isMacintosh && !isWeb ? newUntitledFile : undefined, // fill in one more on macOS to get to 5 entries ]); From 9ceb219f2e1ff88d6eed08ad3dc26734190d2500 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:17:45 -0800 Subject: [PATCH 49/50] fix double shimmer when chat is getting ready and fix tool call icons (#298083) * fix double shimmer when chat is getting ready * fix padding * fix double spinner * remove list renderer changes --- .../browser/widget/chatContentParts/chatProgressContentPart.ts | 3 ++- .../browser/widget/chatContentParts/chatThinkingContentPart.ts | 3 ++- .../widget/chatContentParts/media/chatThinkingContent.css | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts index 95b0edf8e267b..615ae0c63dcdf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts @@ -67,7 +67,8 @@ export class ChatProgressContentPart extends Disposable implements IChatContentP alert(progress.content.value); } const isLoadingIcon = icon && ThemeIcon.isEqual(icon, ThemeIcon.modify(Codicon.loading, 'spin')); - const useShimmer = shimmer ?? ((!icon || isLoadingIcon) && this.showSpinner); + // Even if callers request shimmer, only the active (spinner-visible) progress row should animate. + const useShimmer = (shimmer ?? (!icon || isLoadingIcon)) && this.showSpinner; // if we have shimmer, don't show spinner const codicon = useShimmer ? Codicon.check : (icon ?? (this.showSpinner ? ThemeIcon.modify(Codicon.loading, 'spin') : Codicon.check)); const result = this.chatContentMarkdownRenderer.render(progress.content); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 4a99a9d93bfac..b47f7312a4c76 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -66,7 +66,8 @@ export function getToolInvocationIcon(toolId: string): ThemeIcon { if ( lowerToolId.includes('edit') || - lowerToolId.includes('create') + lowerToolId.includes('create') || + lowerToolId.includes('replace') ) { return Codicon.pencil; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 9cc43bb069933..31acb4c321b58 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -184,7 +184,7 @@ } .chat-thinking-tool-wrapper .chat-markdown-part.rendered-markdown { - padding: 5px 12px 4px 20px; + padding: 5px 12px 4px 24px; .status-icon.codicon-check { display: none; From 37f39645643438347f1b5077abdaa96e7297fa7c Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Thu, 26 Feb 2026 18:42:11 -0800 Subject: [PATCH 50/50] Run oss tool for 1.110 (before branch) (#298147) * Run oss tool for 110 (before branch) * Update distro hash --- cglicenses.json | 58 ---------------- cli/ThirdPartyNotices.txt | 140 ++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 141 insertions(+), 59 deletions(-) diff --git a/cglicenses.json b/cglicenses.json index 2b1bc6fece5b6..37bba3145ba11 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -707,64 +707,6 @@ "For more information, please refer to " ] }, - { - "name": "@isaacs/balanced-match", - "fullLicenseText": [ - "MIT License", - "", - "Copyright Isaac Z. Schlueter ", - "", - "Original code Copyright Julian Gruber ", - "", - "Port to TypeScript Copyright Isaac Z. Schlueter ", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy of", - "this software and associated documentation files (the \"Software\"), to deal in", - "the Software without restriction, including without limitation the rights to", - "use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies", - "of the Software, and to permit persons to whom the Software is furnished to do", - "so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE.", - "" - ] - }, - { - "name": "@isaacs/brace-expansion", - "fullLicenseText": [ - "MIT License", - "", - "Copyright (c) 2013 Julian Gruber ", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE.", - "" - ] - }, { // Reason: License file starts with (MIT) before the copyright, tool can't parse it "name": "balanced-match", diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 61cacaea79978..9edb0ae9d2330 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -9189,6 +9189,32 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +serde_spanned 1.0.4 - MIT OR Apache-2.0 +https://github.com/toml-rs/toml + +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + serde_urlencoded 0.7.1 - MIT/Apache-2.0 https://github.com/nox/serde_urlencoded @@ -10517,7 +10543,34 @@ SOFTWARE. --------------------------------------------------------- +toml 0.9.12+spec-1.1.0 - MIT OR Apache-2.0 +https://github.com/toml-rs/toml + +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + toml_datetime 0.6.11 - MIT OR Apache-2.0 +toml_datetime 0.7.5+spec-1.1.0 - MIT OR Apache-2.0 https://github.com/toml-rs/toml ../../LICENSE-MIT @@ -10533,6 +10586,58 @@ https://github.com/toml-rs/toml --------------------------------------------------------- +toml_parser 1.0.9+spec-1.1.0 - MIT OR Apache-2.0 +https://github.com/toml-rs/toml + +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +toml_writer 1.0.6+spec-1.1.0 - MIT OR Apache-2.0 +https://github.com/toml-rs/toml + +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + tower-service 0.3.3 - MIT https://github.com/tower-rs/tower @@ -12700,6 +12805,7 @@ MIT License --------------------------------------------------------- winnow 0.5.40 - MIT +winnow 0.7.14 - MIT https://github.com/winnow-rs/winnow The MIT License (MIT) @@ -12755,6 +12861,40 @@ THE SOFTWARE. --------------------------------------------------------- +winresource 0.1.30 - MIT +https://github.com/BenjaminRi/winresource + +The MIT License (MIT) + +Copyright 2016 Max Resch + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + wit-bindgen 0.51.0 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT https://github.com/bytecodealliance/wit-bindgen diff --git a/package.json b/package.json index a8b3853221492..67e53ae161026 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "11e5d56acfb25db3d8e0088b0150ce2488eddb53", + "distro": "3ddf7ca3e6b5b372de64cc436eb175522d30f8ab", "author": { "name": "Microsoft Corporation" },