From c4484618330e9c1e31dac6ece8de2d8b45c111f1 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 28 May 2026 22:55:41 +1000 Subject: [PATCH 1/8] feat: enhance session changeset handling with deferred refresh logic (#318733) * feat: enhance session changeset handling with deferred refresh logic * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../node/agentHostChangesetCoordinator.ts | 45 +++++++++++++------ .../agentHostChangesetCoordinator.test.ts | 34 +++++++++++++- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts b/src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts index 5bde8ef302377..ae70dd72b1ba1 100644 --- a/src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts +++ b/src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts @@ -56,9 +56,9 @@ export const CHANGESET_DB_METADATA_KEYS: Record = { * {@link IAgentHostChangesetService} (which owns compute / publish / * persist primitives). * - * Owns the deferred uncommitted-refresh state machine — refreshes that - * fire before the session's working directory is known are queued and - * drained from {@link onSessionMaterialized} / {@link onSessionRestored}. + * Owns the deferred static-refresh state machine — refreshes that fire + * before the session's working directory is known are queued and drained + * from {@link onSessionMaterialized} / {@link onSessionRestored}. * * No per-session controllers — the cross-cutting concerns (listSessions * overlay, subscribe URI routing) inherently span sessions, so a single @@ -73,6 +73,12 @@ export class ChangesetSessionCoordinator extends Disposable { * {@link onSessionRestored} once the working directory is set. */ private readonly _pendingUncommittedRefreshes = new Set(); + /** + * Sessions that subscribed to their session-wide branch changeset before + * the working directory was known. Drained alongside uncommitted refreshes + * once restore / materialization has populated the session summary. + */ + private readonly _pendingSessionRefreshes = new Set(); /** * Per-session set of turn ids that have at least one live subscriber to @@ -148,7 +154,7 @@ export class ChangesetSessionCoordinator extends Disposable { /** * Called when a provisional session is materialized (working directory - * becomes known). Drains any uncommitted refresh that was deferred + * becomes known). Drains any static changeset refresh that was deferred * because the working directory was not yet known. */ onSessionMaterialized(sessionStr: string): void { @@ -162,6 +168,7 @@ export class ChangesetSessionCoordinator extends Disposable { */ onSessionDisposed(sessionStr: string): void { this._pendingUncommittedRefreshes.delete(sessionStr); + this._pendingSessionRefreshes.delete(sessionStr); this._subscribedTurns.delete(sessionStr); this._changesetFileMonitor.onSessionDisposed(sessionStr); } @@ -173,9 +180,9 @@ export class ChangesetSessionCoordinator extends Disposable { // ---- Subscription hooks ------------------------------------------------- /** - * Called on every `addSubscriber` 0→1 transition. When `resource` is - * the uncommitted changeset URI, triggers the first git-diff refresh - * (or queues it for later if the working directory is not yet known). + * Called on every `addSubscriber` 0→1 transition. When `resource` is a + * static changeset URI, triggers the first git-diff refresh (or queues + * it for later if the working directory is not yet known). * * Both {@link AgentService.subscribe} and the handshake fast-path * (`ProtocolServerHandler.initialSubscriptions`) call into @@ -190,10 +197,7 @@ export class ChangesetSessionCoordinator extends Disposable { return; } if (parsed?.kind === ChangesetKind.Session) { - // Session-changeset compute uses git when a working dir is - // available and falls back to the SDK edit-tracker otherwise, - // so it doesn't need the same deferral as uncommitted. - this._changesets.refreshSessionChangeset(parsed.sessionUri); + this._triggerSessionRefresh(parsed.sessionUri); this._changesetFileMonitor.trackSessionChanges(resourceStr, parsed.sessionUri); return; } @@ -219,15 +223,15 @@ export class ChangesetSessionCoordinator extends Disposable { // to the changeset URIs directly, and the user has been // editing files manually in the working tree. this._triggerUncommittedRefresh(resourceStr); - this._changesets.refreshSessionChangeset(resourceStr); + this._triggerSessionRefresh(resourceStr); this._changesetFileMonitor.trackSessionChanges(resourceStr, resourceStr); } } /** * Called when a resource's last subscriber drops. Cleans up any - * deferred uncommitted refresh queued for that session — if no one is - * subscribed anymore, there's no point firing it on materialize. + * deferred refresh queued for that session — if no one is subscribed anymore, + * there's no point firing it on materialize. */ onLastSubscriber(resource: URI): void { const resourceStr = resource.toString(); @@ -238,6 +242,7 @@ export class ChangesetSessionCoordinator extends Disposable { return; } if (parsed?.kind === ChangesetKind.Session) { + this._pendingSessionRefreshes.delete(parsed.sessionUri); this._changesetFileMonitor.untrackSessionChanges(resourceStr); return; } @@ -432,9 +437,21 @@ export class ChangesetSessionCoordinator extends Disposable { this._changesets.refreshUncommittedChangeset(sessionStr); } + private _triggerSessionRefresh(sessionStr: string): void { + const wd = this._configurationService.getEffectiveWorkingDirectory(sessionStr); + if (!wd) { + this._pendingSessionRefreshes.add(sessionStr); + return; + } + this._changesets.refreshSessionChangeset(sessionStr); + } + private _drainPendingRefresh(sessionStr: string): void { if (this._pendingUncommittedRefreshes.delete(sessionStr)) { this._triggerUncommittedRefresh(sessionStr); } + if (this._pendingSessionRefreshes.delete(sessionStr)) { + this._triggerSessionRefresh(sessionStr); + } } } diff --git a/src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts b/src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts index 84aeaceb4389a..5579b628aaa25 100644 --- a/src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts @@ -10,7 +10,7 @@ import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { NullLogService } from '../../../log/common/log.js'; import { AgentSession, IAgentSessionMetadata } from '../../common/agentService.js'; -import { buildDefaultChangesetCatalogue, buildUncommittedChangesetUri } from '../../common/changesetUri.js'; +import { buildDefaultChangesetCatalogue, buildSessionChangesetUri, buildUncommittedChangesetUri } from '../../common/changesetUri.js'; import { ActionType } from '../../common/state/sessionActions.js'; import { buildSubagentSessionUri, SessionStatus, type ISessionFileDiff } from '../../common/state/sessionState.js'; import { AgentConfigurationService } from '../../node/agentConfigurationService.js'; @@ -127,6 +127,38 @@ suite('ChangesetSessionCoordinator', () => { }); }); + test('defers session changeset refresh until the working directory is known', async () => { + const session = AgentSession.uri('mock', 'session-1').toString(); + const environment = createEnvironment(); + createSession(environment.stateManager, session, undefined, false); + + environment.coordinator.onFirstSubscriber(URI.parse(buildSessionChangesetUri(session))); + await tick(); + + const summary = environment.stateManager.getSessionState(session)!.summary; + environment.stateManager.markSessionPersisted(session, { ...summary, workingDirectory: 'file:///repo/worktree' }); + environment.coordinator.onSessionMaterialized(session); + await tick(); + + assert.deepStrictEqual(environment.changesets.sessionRefreshes, [session]); + }); + + test('drops pending session changeset refresh when the last subscriber leaves', async () => { + const session = AgentSession.uri('mock', 'session-1').toString(); + const environment = createEnvironment(); + const changeset = buildSessionChangesetUri(session); + createSession(environment.stateManager, session, undefined, false); + + environment.coordinator.onFirstSubscriber(URI.parse(changeset)); + environment.coordinator.onLastSubscriber(URI.parse(changeset)); + const summary = environment.stateManager.getSessionState(session)!.summary; + environment.stateManager.markSessionPersisted(session, { ...summary, workingDirectory: 'file:///repo/worktree' }); + environment.coordinator.onSessionMaterialized(session); + await tick(); + + assert.deepStrictEqual(environment.changesets.sessionRefreshes, []); + }); + test('does not attach root state when watcher acquisition fails', async () => { const session = AgentSession.uri('mock', 'session-1').toString(); const environment = createEnvironment(); From d8aa32df0c2755b05f47d56e76887ea327029104 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 14:58:57 +0200 Subject: [PATCH 2/8] Fix 2px double border above account panel content in Agents window (#318649) Fix double 2px border above account panel content Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eli-w-king <201316543+eli-w-king@users.noreply.github.com> --- .../contrib/accountMenu/browser/media/accountTitleBarWidget.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/sessions/contrib/accountMenu/browser/media/accountTitleBarWidget.css b/src/vs/sessions/contrib/accountMenu/browser/media/accountTitleBarWidget.css index cbe0a33098778..dca6578d1c455 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/media/accountTitleBarWidget.css +++ b/src/vs/sessions/contrib/accountMenu/browser/media/accountTitleBarWidget.css @@ -224,7 +224,6 @@ display: flex; flex-direction: column; min-height: 0; - border-top: 1px solid var(--vscode-menu-separatorBackground, var(--vscode-disabledForeground)); } .agent-sessions-workbench .sessions-account-titlebar-panel-summary { From b528e0633fd9124879f1c5c86e9a2db267acb2f9 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 28 May 2026 14:24:53 +0100 Subject: [PATCH 3/8] Update @vscode/codicons to version 0.0.46-14 and add cloudCompact icon (#318717) feat: update @vscode/codicons to version 0.0.46-14 and add cloudCompact icon Co-authored-by: mrleemurray --- package-lock.json | 8 ++++---- package.json | 2 +- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- src/vs/base/common/codiconsLibrary.ts | 1 + 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96d43ce51a502..1d907f3c32879 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@microsoft/mxc-sdk": "0.2.1", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.46-13", + "@vscode/codicons": "^0.0.46-14", "@vscode/copilot-api": "^0.4.1", "@vscode/deviceid": "^0.1.1", "@vscode/diff": "^0.0.2-0", @@ -3521,9 +3521,9 @@ } }, "node_modules/@vscode/codicons": { - "version": "0.0.46-13", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.46-13.tgz", - "integrity": "sha512-2nlRwaYDGiP19+GRPuoOx6DMDLBc35BsiLjCxKZPTNtHnS6rNpdm/Apa4HGt5+mFPB43nfxL2rcbAczso9KxQQ==", + "version": "0.0.46-14", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.46-14.tgz", + "integrity": "sha512-jeiVtypLqoytNMG62mvWDRFBx76lGiJDbuXVdnJJmZAZ9DVXbanTWlsyIvaAtoGhMMgONYYvZbdZfS90VMdr2Q==", "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { diff --git a/package.json b/package.json index 7815d6980dd6f..6c59ce2b57442 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "@microsoft/mxc-sdk": "0.2.1", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.46-13", + "@vscode/codicons": "^0.0.46-14", "@vscode/copilot-api": "^0.4.1", "@vscode/deviceid": "^0.1.1", "@vscode/diff": "^0.0.2-0", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 33cd6aa63fa5e..fedd410399a6e 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.46-13", + "@vscode/codicons": "^0.0.46-14", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.1", "@vscode/vscode-languagedetection": "1.0.23", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.46-13", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.46-13.tgz", - "integrity": "sha512-2nlRwaYDGiP19+GRPuoOx6DMDLBc35BsiLjCxKZPTNtHnS6rNpdm/Apa4HGt5+mFPB43nfxL2rcbAczso9KxQQ==", + "version": "0.0.46-14", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.46-14.tgz", + "integrity": "sha512-jeiVtypLqoytNMG62mvWDRFBx76lGiJDbuXVdnJJmZAZ9DVXbanTWlsyIvaAtoGhMMgONYYvZbdZfS90VMdr2Q==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index 4596a81e98c1d..ab45193fe640c 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.46-13", + "@vscode/codicons": "^0.0.46-14", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.1", "@vscode/vscode-languagedetection": "1.0.23", diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index a66913904f522..43f9b30610972 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -714,4 +714,5 @@ export const codiconsLibrary = { vmPending: register('vm-pending', 0xecbc), worktreeCompact: register('worktree-compact', 0xecbd), developerTools: register('developer-tools', 0xecbe), + cloudCompact: register('cloud-compact', 0xecbf), } as const; From c1b72a2e202b3dc02e7801a61e30ca9d77586b61 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Thu, 28 May 2026 18:27:29 +0500 Subject: [PATCH 4/8] Fix /doc placing Python docstrings before decorators (#318571) * Fix /doc placing Python docstrings before decorators (#283165) In Python's tree-sitter grammar, a `decorated_definition` node wraps `function_definition` / `class_definition`. The previous fall-through to the default regex `/definition|declaration|declarator/` matched the outer `decorated_definition`, so the documentable node's range started at the `@decorator` line. Downstream consumers (LLM prompt context, docstring insertion anchor) then placed docstrings *above* the a Pylance error and broken code.decorator Match only the inner `function_definition` / `class_definition` for Python. Add unit tests covering plain/decorated functions, classes, and methods (including stacked decorators and decorated classes). Fixes #283165 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: handle cursor-on-decorator for Python /doc Address review feedback on #283165: - Match `decorated_definition` in `isDocumentableNode` for Python so cursors/selections on the `@decorator` line no longer escape past it to the `module` root. - Introduce `unwrapPythonDecoratedDefinition` and call it in both code paths of `_getNodeToDocument` so the returned range is always the inner `function_definition`/`class_ excluding the decorator definition` regardless of whether the node was found via selection match or via walk-up. - Fix misleading test title for whole-method decorated selection (now picks the inner function_definition as advertised). - Add 4 regression tests for cursor-on-decorator (incl. on `@` sign, selection of decorator-only, decorator on method inside class). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: unwrap decorated_definition in _getDocumentableNodeIfOnIdentifier too Defensive consistency fix from subagent code review. While the Python tree-sitter grammar makes `function_definition`/`class_definition` (not `decorated_definition`) the direct parent of name so thisidentifiers branch can't trigger applying the unwrap maintains thetoday invariant: "the parser API never exposes a Python `decorated_definition` externally". This protects against future grammar changes or callers that pass ranges over decorator children. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/platform/parser/node/docGenParsing.ts | 13 +- .../copilot/src/platform/parser/node/util.ts | 30 ++ .../test/node/getNodeToDocument.py.spec.ts | 349 ++++++++++++++++++ 3 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 extensions/copilot/src/platform/parser/test/node/getNodeToDocument.py.spec.ts diff --git a/extensions/copilot/src/platform/parser/node/docGenParsing.ts b/extensions/copilot/src/platform/parser/node/docGenParsing.ts index 72cb5d5b7ec9d..30cbed28bfbd9 100644 --- a/extensions/copilot/src/platform/parser/node/docGenParsing.ts +++ b/extensions/copilot/src/platform/parser/node/docGenParsing.ts @@ -8,7 +8,7 @@ import { Node, TreeSitterOffsetRange } from './nodes'; import { _parse } from './parserWithCaching'; import { _getNodeMatchingSelection } from './selectionParsing'; import { WASMLanguage } from './treeSitterLanguages'; -import { extractIdentifier, isDocumentableNode } from './util'; +import { extractIdentifier, isDocumentableNode, unwrapPythonDecoratedDefinition } from './util'; export type NodeToDocumentContext = { @@ -53,10 +53,11 @@ export async function _getNodeToDocument( const selectionMatchedNode = isSelectionEmpty ? undefined : _getNodeMatchingSelection(treeRef.tree, selection, language); if (selectionMatchedNode) { - const nodeIdentifier = extractIdentifier(selectionMatchedNode, language); + const unwrapped = unwrapPythonDecoratedDefinition(selectionMatchedNode, language); + const nodeIdentifier = extractIdentifier(unwrapped, language); return { nodeIdentifier, - nodeToDocument: Node.ofSyntaxNode(selectionMatchedNode), + nodeToDocument: Node.ofSyntaxNode(unwrapped), nodeSelectionBy: 'matchingSelection' }; } @@ -72,6 +73,8 @@ export async function _getNodeToDocument( ++nNodesClimbedUp; } + nodeToDocument = unwrapPythonDecoratedDefinition(nodeToDocument, language); + const nodeIdentifier = extractIdentifier(nodeToDocument, language); return { nodeIdentifier, @@ -96,7 +99,9 @@ export async function _getDocumentableNodeIfOnIdentifier( if (smallestNodeContainingRange.type.match(/identifier/) && (smallestNodeContainingRange.parent === null || isDocumentableNode(smallestNodeContainingRange.parent, language)) ) { - const parent = smallestNodeContainingRange.parent; + const parent = smallestNodeContainingRange.parent === null + ? null + : unwrapPythonDecoratedDefinition(smallestNodeContainingRange.parent, language); const parentNodeRange = parent === null ? undefined diff --git a/extensions/copilot/src/platform/parser/node/util.ts b/extensions/copilot/src/platform/parser/node/util.ts index 2754cb45f8e25..7edceb3e78a12 100644 --- a/extensions/copilot/src/platform/parser/node/util.ts +++ b/extensions/copilot/src/platform/parser/node/util.ts @@ -75,7 +75,37 @@ export function isDocumentableNode(node: SyntaxNode, language: WASMLanguage) { return node.type.match(/definition|declaration|class_specifier/); case WASMLanguage.Ruby: return node.type.match(/module|class|method|assignment/); + case WASMLanguage.Python: + // Match `function_definition`/`class_definition`, plus the wrapping + // `decorated_definition` so that selections/cursors *on the decorator + // line itself* still resolve to the function/class (rather than + // climbing all the way up to `module`). Callers must unwrap a matched + // `decorated_definition` to its inner definition via + // {@link unwrapPythonDecoratedDefinition} so that downstream consumers + // (e.g. docstring generation) treat the `def`/`class` line — not the + // `@decorator` line — as the start of the definition. Otherwise + // docstrings end up *before* the decorator, which is a syntax error. + // See https://github.com/microsoft/vscode/issues/283165. + return node.type.match(/^(function_definition|class_definition|decorated_definition)$/); default: return node.type.match(/definition|declaration|declarator/); } } + +/** + * In Python, a `decorated_definition` wraps a `function_definition` or + * `class_definition` whenever decorators are present. Its range starts at the + * `@decorator` line, which is *not* what callers want as "the node to + * document" — placing a docstring at that range would put it before the + * decorator (a syntax error). This helper unwraps a `decorated_definition` to + * its inner `function_definition`/`class_definition` so the range starts at + * the `def`/`class` keyword. + * + * See {@link isDocumentableNode} and https://github.com/microsoft/vscode/issues/283165. + */ +export function unwrapPythonDecoratedDefinition(node: SyntaxNode, language: WASMLanguage): SyntaxNode { + if (language !== WASMLanguage.Python || node.type !== 'decorated_definition') { + return node; + } + return node.children.find(c => c.type === 'function_definition' || c.type === 'class_definition') ?? node; +} diff --git a/extensions/copilot/src/platform/parser/test/node/getNodeToDocument.py.spec.ts b/extensions/copilot/src/platform/parser/test/node/getNodeToDocument.py.spec.ts new file mode 100644 index 0000000000000..1b918a77f2901 --- /dev/null +++ b/extensions/copilot/src/platform/parser/test/node/getNodeToDocument.py.spec.ts @@ -0,0 +1,349 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { outdent } from 'outdent'; +import { afterAll, expect, suite, test } from 'vitest'; +import { + _dispose +} from '../../node/parserImpl'; +import { WASMLanguage } from '../../node/treeSitterLanguages'; +import { srcWithAnnotatedNodeToDoc } from './getNodeToDocument.util'; + + +suite('getNodeToDocument - python', () => { + + afterAll(() => _dispose()); + + async function run(annotatedSrc: string) { + return srcWithAnnotatedNodeToDoc( + WASMLanguage.Python, + annotatedSrc, + ); + } + + test('plain function definition - cursor on `def`', async () => { + const result = await run( + outdent` + <> hello(): + return 1 + `, + ); + expect(result).toMatchInlineSnapshot(` + "def hello(): + return 1" + `); + }); + + test('plain function definition - cursor in body', async () => { + const result = await run( + outdent` + def hello(): + return <<1>> + `, + ); + expect(result).toMatchInlineSnapshot(` + "def hello(): + return 1" + `); + }); + + test('plain class definition - cursor on `class`', async () => { + const result = await run( + outdent` + <> Foo: + x = 1 + `, + ); + expect(result).toMatchInlineSnapshot(` + "class Foo: + x = 1" + `); + }); + + // Regression test for https://github.com/microsoft/vscode/issues/283165: + // when a Python function has a decorator, the documentable node should be + // the inner `function_definition` (not the wrapping `decorated_definition`), + // so that anything downstream (LLM prompt context, docstring insertion) + // treats the `def` line — not the `@decorator` line — as the start of the + // function. Otherwise docstrings end up *before* the decorator, which is a + // syntax error / Pylance error. + test('decorated function - cursor in body picks function_definition (not decorated_definition)', async () => { + const result = await run( + outdent` + import pytest + + @pytest.fixture(scope="module") + def sample_data(): + return <<{}>> + `, + ); + expect(result).toMatchInlineSnapshot(` + "import pytest + + @pytest.fixture(scope="module") + def sample_data(): + return {}" + `); + }); + + test('decorated function - cursor on `def` picks function_definition', async () => { + const result = await run( + outdent` + @my_decorator + <> say_hello(): + print("hi") + `, + ); + expect(result).toMatchInlineSnapshot(` + "@my_decorator + def say_hello(): + print("hi")" + `); + }); + + test('decorated function - selecting the whole file picks function_definition', async () => { + const result = await run( + outdent` + <<@my_decorator + def say_hello(): + print("hi") + >>`, + ); + expect(result).toMatchInlineSnapshot(` + "@my_decorator + def say_hello(): + print("hi") + " + `); + }); + + test('multiple decorators - picks function_definition', async () => { + const result = await run( + outdent` + @first + @second(arg=1) + @third + def stacked(): + return <<42>> + `, + ); + expect(result).toMatchInlineSnapshot(` + "@first + @second(arg=1) + @third + def stacked(): + return 42" + `); + }); + + test('decorated class - picks class_definition (not decorated_definition)', async () => { + const result = await run( + outdent` + @dataclass + class Point: + x: int = <<0>> + y: int = 0 + `, + ); + expect(result).toMatchInlineSnapshot(` + "@dataclass + class Point: + x: int = 0 + y: int = 0" + `); + }); + + test('decorated method inside a class - picks the inner function_definition', async () => { + const result = await run( + outdent` + class Service: + @staticmethod + def helper(): + return <<42>> + `, + ); + expect(result).toMatchInlineSnapshot(` + "class Service: + @staticmethod + def helper(): + return 42" + `); + }); + + test('plain method inside a class - cursor in body picks function_definition (not class_definition)', async () => { + const result = await run( + outdent` + class Service: + def helper(self): + return <<42>> + `, + ); + expect(result).toMatchInlineSnapshot(` + "class Service: + def helper(self): + return 42" + `); + }); + + test('decorated method - whole-method selection picks the inner function_definition', async () => { + const result = await run( + outdent` + class Service: + <<@staticmethod + def helper(): + return 42>> + `, + ); + expect(result).toMatchInlineSnapshot(` + "class Service: + @staticmethod + def helper(): + return 42" + `); + }); + + test('decorated method inside a decorated class - cursor in method body picks the method function_definition', async () => { + const result = await run( + outdent` + @dataclass + class Service: + @staticmethod + def helper(): + return <<42>> + `, + ); + expect(result).toMatchInlineSnapshot(` + "@dataclass + class Service: + @staticmethod + def helper(): + return 42" + `); + }); + + test('classmethod with @property-like decorator - picks the method function_definition', async () => { + const result = await run( + outdent` + class Config: + @classmethod + @cached + def from_env(cls): + return <> + `, + ); + expect(result).toMatchInlineSnapshot(` + "class Config: + @classmethod + @cached + def from_env(cls): + return cls()" + `); + }); + + // Reproduces the exact scenario from + // https://github.com/microsoft/vscode/issues/283165: a Python file with an + // import and a single decorated function, with the user selecting the entire + // file content. The node-to-document must be the inner `function_definition` + // so that any docstring is inserted *inside* the function (after `def …:`), + // not above the `@pytest.fixture(...)` decorator. + test('issue #283165: decorated pytest fixture with whole-file selection', async () => { + const result = await run( + outdent` + <>`, + ); + expect(result).toMatchInlineSnapshot(` + "import pytest + + @pytest.fixture(scope="module") + def sample_data(): + return { + "name": "John Doe", + "age": 30, + "email": "john.doe@example.com" + } + " + `); + }); + + // Regression test: when the cursor is *on the decorator line*, the + // node-to-document must still be the inner function/class (with the range + // excluding the `@decorator`). Previously this would walk all the way up + // to the `module` node, which is a regression for `/doc` invoked with the + // cursor on `@decorator`. + test('decorated function - cursor on decorator picks function_definition (not module)', async () => { + const result = await run( + outdent` + import pytest + + @pytest.fix<<>>ture(scope="module") + def sample_data(): + return {} + `, + ); + expect(result).toMatchInlineSnapshot(` + "import pytest + + @pytest.fixture(scope="module") + def sample_data(): + return {}" + `); + }); + + test('decorated function - cursor on the `@` sign picks function_definition (not module)', async () => { + const result = await run( + outdent` + <<@>>my_decorator + def say_hello(): + print("hi") + `, + ); + expect(result).toMatchInlineSnapshot(` + "@my_decorator + def say_hello(): + print("hi")" + `); + }); + + test('decorated function - selection covering only the decorator line picks function_definition', async () => { + const result = await run( + outdent` + <<@my_decorator>> + def say_hello(): + print("hi") + `, + ); + expect(result).toMatchInlineSnapshot(` + "@my_decorator + def say_hello(): + print("hi")" + `); + }); + + test('decorated method - cursor on decorator inside a class picks the method function_definition', async () => { + const result = await run( + outdent` + class Service: + @static<<>>method + def helper(): + return 42 + `, + ); + expect(result).toMatchInlineSnapshot(` + "class Service: + @staticmethod + def helper(): + return 42" + `); + }); +}); From 69115383cf36d377170e4ad22c73ead7ac330bd5 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Thu, 28 May 2026 18:28:01 +0500 Subject: [PATCH 5/8] xtab: add PatchBased02 line-number variants and strategy-defaults map (#318718) * xtab: add PatchBased02 line-number variants and strategy-defaults map Introduce two new prompting strategies that bake their configuration into the strategy itself: - PatchBased02WithRecentLineNumbers - PatchBased02WithoutRecentLineNumbers Both share the PatchBased02 prompt/response format with currentFile line numbers in 'withoutSpaceAfter' style, no current-file tags, postscript enabled, and next-cursor-line prediction disabled. They differ only in whether recently-viewed documents include line numbers. Add a STRATEGY_CONFIG map and applyStrategyConfig() helper that overlay strategy-specific defaults onto the upstream model configuration. Fold the existing CopilotNesXtab includeTagsInCurrentFile hack into this map so there is a single source of truth for per-strategy baked-in defaults. * address review: preserve undefined for unset option bags; add tests --- .../extension/xtab/common/promptCrafting.ts | 10 ++- .../src/extension/xtab/node/xtabProvider.ts | 7 +- .../xtab/test/node/xtabProvider.spec.ts | 2 + .../common/dataTypes/xtabPromptOptions.ts | 50 +++++++++++++ .../test/common/xtabPromptOptions.spec.ts | 75 +++++++++++++++++++ 5 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts diff --git a/extensions/copilot/src/extension/xtab/common/promptCrafting.ts b/extensions/copilot/src/extension/xtab/common/promptCrafting.ts index c95de285a8a06..ae8dd1d54bc2e 100644 --- a/extensions/copilot/src/extension/xtab/common/promptCrafting.ts +++ b/extensions/copilot/src/extension/xtab/common/promptCrafting.ts @@ -109,7 +109,9 @@ ${PromptTags.EDIT_HISTORY.end}`; case PromptingStrategy.PatchBased01: mainPrompt = basePrompt; break; - case PromptingStrategy.PatchBased02: { + case PromptingStrategy.PatchBased02: + case PromptingStrategy.PatchBased02WithRecentLineNumbers: + case PromptingStrategy.PatchBased02WithoutRecentLineNumbers: { const currentDocument = promptPieces.currentDocument; const cursorLine = currentDocument.lineWithCursor(); const cursorLineWithTag = cursorLine.substring(0, currentDocument.cursorPosition.column - 1) + PromptTags.CURSOR + cursorLine.substring(currentDocument.cursorPosition.column - 1); @@ -144,7 +146,9 @@ ${PromptTags.EDIT_HISTORY.end}`; const includeBackticks = opts.promptingStrategy !== PromptingStrategy.Nes41Miniv3 && opts.promptingStrategy !== PromptingStrategy.Codexv21NesUnified && opts.promptingStrategy !== PromptingStrategy.PatchBased01 && - opts.promptingStrategy !== PromptingStrategy.PatchBased02; + opts.promptingStrategy !== PromptingStrategy.PatchBased02 && + opts.promptingStrategy !== PromptingStrategy.PatchBased02WithRecentLineNumbers && + opts.promptingStrategy !== PromptingStrategy.PatchBased02WithoutRecentLineNumbers; const packagedPrompt = includeBackticks ? wrapInBackticks(mainPrompt) : mainPrompt; const packagedPromptWithRelatedInfo = addRelatedInformation(relatedInformation, packagedPrompt, opts.languageContext.traitPosition); @@ -331,6 +335,8 @@ function getPostScript(strategy: PromptingStrategy | undefined, currentFilePath: case PromptingStrategy.Codexv21NesUnified: break; case PromptingStrategy.PatchBased02: + case PromptingStrategy.PatchBased02WithRecentLineNumbers: + case PromptingStrategy.PatchBased02WithoutRecentLineNumbers: postScript = `The developer was working on a section of code within the \`current_file_content\` - carefully note their \`cursor_location\` marked with \`<|cursor|>\`. Using the given \`recently_viewed_code_snippets\`, \`current_file_content\`, \`edit_diff_history\`, and \`cursor_location\`, please continue the developer's work. Output a modified diff format with a sequence of intuitive next changes, where each patch must start with \`:\`. Order changes by priority and flow; for instance, edits adjacent to the user's cursor should always be prioritized, followed by lines near the cursor, followed by lines farther away. If there are no good edit candidates, output the empty string "". Avoid undoing or reverting the developer's last change unless there are obvious typos or errors. Adhere meticulously to the diff format.`; break; case PromptingStrategy.UnifiedModel: diff --git a/extensions/copilot/src/extension/xtab/node/xtabProvider.ts b/extensions/copilot/src/extension/xtab/node/xtabProvider.ts index cdc9d03ca3409..cc4dbf50598cc 100644 --- a/extensions/copilot/src/extension/xtab/node/xtabProvider.ts +++ b/extensions/copilot/src/extension/xtab/node/xtabProvider.ts @@ -1451,10 +1451,7 @@ export class XtabProvider implements IStatelessNextEditProvider { }; const selectedModelConfig = this.modelService.selectedModelConfiguration(); - // proxy /models doesn't know about includeTagsInCurrentFile field as of now, so hard code it to true for CopilotNesXtab strategy - const modelConfig: xtabPromptOptions.ModelConfiguration = selectedModelConfig.promptingStrategy === xtabPromptOptions.PromptingStrategy.CopilotNesXtab - ? { ...selectedModelConfig, includeTagsInCurrentFile: true } - : selectedModelConfig; + const modelConfig = xtabPromptOptions.applyStrategyConfig(selectedModelConfig); return { promptOptions: overrideModelConfig(sourcedModelConfig, modelConfig), modelServiceConfig: modelConfig @@ -1654,6 +1651,8 @@ export function pickSystemPrompt(promptingStrategy: xtabPromptOptions.PromptingS case xtabPromptOptions.PromptingStrategy.PatchBased: case xtabPromptOptions.PromptingStrategy.PatchBased01: case xtabPromptOptions.PromptingStrategy.PatchBased02: + case xtabPromptOptions.PromptingStrategy.PatchBased02WithRecentLineNumbers: + case xtabPromptOptions.PromptingStrategy.PatchBased02WithoutRecentLineNumbers: case xtabPromptOptions.PromptingStrategy.Xtab275: case xtabPromptOptions.PromptingStrategy.XtabAggressiveness: case xtabPromptOptions.PromptingStrategy.Xtab275Aggressiveness: diff --git a/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts b/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts index 3bc065976a4cb..f059346d8aa49 100644 --- a/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts +++ b/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts @@ -162,6 +162,8 @@ describe('pickSystemPrompt', () => { PromptingStrategy.PatchBased, PromptingStrategy.PatchBased01, PromptingStrategy.PatchBased02, + PromptingStrategy.PatchBased02WithRecentLineNumbers, + PromptingStrategy.PatchBased02WithoutRecentLineNumbers, PromptingStrategy.Xtab275, PromptingStrategy.XtabAggressiveness, PromptingStrategy.Xtab275Aggressiveness, diff --git a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts index 12ea17e4ae1d4..05baeeb203672 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts @@ -343,6 +343,10 @@ export enum PromptingStrategy { PatchBased = 'patchBased', PatchBased01 = 'patchBased01', PatchBased02 = 'patchBased02', + /** PatchBased02 variant: line numbers on recent docs. */ + PatchBased02WithRecentLineNumbers = 'patchBased02WithRecentLineNumbers', + /** PatchBased02 variant: no line numbers on recent docs. */ + PatchBased02WithoutRecentLineNumbers = 'patchBased02WithoutRecentLineNumbers', /** * Xtab275-based strategy with edit intent tag parsing. * Response format: <|edit_intent|>low|medium|high|no_edit<|/edit_intent|> @@ -393,6 +397,8 @@ export namespace ResponseFormat { case PromptingStrategy.PatchBased: case PromptingStrategy.PatchBased01: case PromptingStrategy.PatchBased02: + case PromptingStrategy.PatchBased02WithRecentLineNumbers: + case PromptingStrategy.PatchBased02WithoutRecentLineNumbers: return ResponseFormat.CustomDiffPatch; case PromptingStrategy.Xtab275EditIntent: return ResponseFormat.EditWindowWithEditIntent; @@ -473,6 +479,50 @@ export interface ModelConfiguration { supportsNextCursorLinePrediction?: boolean; } +/** + * Per-strategy configuration baked into the strategy itself. When a strategy + * declares values here, those values override anything provided by the upstream + * model configuration. A strategy without an entry contributes no overrides. + */ +const STRATEGY_CONFIG: Partial>> = { + // proxy /models doesn't know about includeTagsInCurrentFile field as of now, so hard-code it for CopilotNesXtab + [PromptingStrategy.CopilotNesXtab]: { + includeTagsInCurrentFile: true, + }, + [PromptingStrategy.PatchBased02WithRecentLineNumbers]: { + includeTagsInCurrentFile: false, + includePostScript: true, + currentFile: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace }, + recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace }, + supportsNextCursorLinePrediction: false, + }, + [PromptingStrategy.PatchBased02WithoutRecentLineNumbers]: { + includeTagsInCurrentFile: false, + includePostScript: true, + currentFile: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace }, + recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.None }, + supportsNextCursorLinePrediction: false, + }, +}; + +/** Apply per-strategy baked-in config; strategy values override `config`. */ +export function applyStrategyConfig(config: ModelConfiguration): ModelConfiguration { + const overrides = config.promptingStrategy === undefined ? undefined : STRATEGY_CONFIG[config.promptingStrategy]; + if (!overrides) { + return config; + } + const hasCurrentFile = config.currentFile !== undefined || overrides.currentFile !== undefined; + const hasRecentlyViewed = config.recentlyViewedDocuments !== undefined || overrides.recentlyViewedDocuments !== undefined; + const hasLintOptions = config.lintOptions !== undefined || overrides.lintOptions !== undefined; + return { + ...config, + ...overrides, + currentFile: hasCurrentFile ? { ...config.currentFile, ...overrides.currentFile } : undefined, + recentlyViewedDocuments: hasRecentlyViewed ? { ...config.recentlyViewedDocuments, ...overrides.recentlyViewedDocuments } : undefined, + lintOptions: hasLintOptions ? { ...config.lintOptions, ...overrides.lintOptions } : undefined, + }; +} + export const LINT_OPTIONS_VALIDATOR: IValidator> = vObj({ 'tagName': vString(), 'warnings': vEnum(LintOptionWarning.YES, LintOptionWarning.NO, LintOptionWarning.YES_IF_NO_ERRORS), diff --git a/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts b/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts new file mode 100644 index 0000000000000..4a57b3b1d8c0b --- /dev/null +++ b/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from 'vitest'; +import { applyStrategyConfig, IncludeLineNumbersOption, ModelConfiguration, PromptingStrategy } from '../../common/dataTypes/xtabPromptOptions'; + +function baseConfig(overrides: Partial = {}): ModelConfiguration { + return { + modelName: 'test-model', + promptingStrategy: undefined, + includeTagsInCurrentFile: true, + lintOptions: undefined, + ...overrides, + }; +} + +describe('applyStrategyConfig', () => { + + it('returns config unchanged when strategy has no entry', () => { + const config = baseConfig({ promptingStrategy: PromptingStrategy.Xtab275 }); + expect(applyStrategyConfig(config)).toBe(config); + }); + + it('returns config unchanged when strategy is undefined', () => { + const config = baseConfig(); + expect(applyStrategyConfig(config)).toBe(config); + }); + + it('forces includeTagsInCurrentFile=true for CopilotNesXtab', () => { + const result = applyStrategyConfig(baseConfig({ + promptingStrategy: PromptingStrategy.CopilotNesXtab, + includeTagsInCurrentFile: false, + })); + expect(result.includeTagsInCurrentFile).toBe(true); + }); + + it('forces baked-in fields for PatchBased02WithRecentLineNumbers', () => { + const result = applyStrategyConfig(baseConfig({ + promptingStrategy: PromptingStrategy.PatchBased02WithRecentLineNumbers, + includeTagsInCurrentFile: true, + includePostScript: false, + currentFile: { includeLineNumbers: IncludeLineNumbersOption.None, maxTokens: 42 }, + recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.None, maxTokens: 99 }, + supportsNextCursorLinePrediction: true, + })); + expect(result).toMatchObject({ + includeTagsInCurrentFile: false, + includePostScript: true, + currentFile: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace, maxTokens: 42 }, + recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace, maxTokens: 99 }, + supportsNextCursorLinePrediction: false, + }); + }); + + it('forces recentlyViewedDocuments.includeLineNumbers=None for PatchBased02WithoutRecentLineNumbers', () => { + const result = applyStrategyConfig(baseConfig({ + promptingStrategy: PromptingStrategy.PatchBased02WithoutRecentLineNumbers, + recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.WithSpaceAfter }, + })); + expect(result.recentlyViewedDocuments?.includeLineNumbers).toBe(IncludeLineNumbersOption.None); + expect(result.currentFile?.includeLineNumbers).toBe(IncludeLineNumbersOption.WithoutSpace); + }); + + it('preserves undefined for option bags neither side specifies', () => { + const result = applyStrategyConfig(baseConfig({ + promptingStrategy: PromptingStrategy.CopilotNesXtab, + })); + // CopilotNesXtab only sets includeTagsInCurrentFile; nested option bags should stay undefined. + expect(result.currentFile).toBeUndefined(); + expect(result.recentlyViewedDocuments).toBeUndefined(); + expect(result.lintOptions).toBeUndefined(); + }); +}); From 6149246c909a51c53114bd2db6834677bd10e8b2 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 28 May 2026 12:30:06 +0200 Subject: [PATCH 6/8] update vscode diff --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d907f3c32879..05a631f5e97f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@vscode/codicons": "^0.0.46-14", "@vscode/copilot-api": "^0.4.1", "@vscode/deviceid": "^0.1.1", - "@vscode/diff": "^0.0.2-0", + "@vscode/diff": "0.0.2-7", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", "@vscode/policy-watcher": "^1.3.2", @@ -3588,9 +3588,9 @@ } }, "node_modules/@vscode/diff": { - "version": "0.0.2-0", - "resolved": "https://registry.npmjs.org/@vscode/diff/-/diff-0.0.2-0.tgz", - "integrity": "sha512-gmwM9W6mLnqNxcCd0u9WTuL3JJjaAuicoNcPWNEbHFe8OS8SvdQ6q+txVQTwLT6ezUnXQ6e8sQwmjPSE384yxQ==", + "version": "0.0.2-7", + "resolved": "https://registry.npmjs.org/@vscode/diff/-/diff-0.0.2-7.tgz", + "integrity": "sha512-zGPIPeUAmQs79u7g6FTLmhlXFIocUTtuHYmCV5lRZf6vlDk7SWEOpBlYY6SsShC3TB/lm2KxXMj9tP32YOPrhg==", "license": "MIT" }, "node_modules/@vscode/gulp-electron": { diff --git a/package.json b/package.json index 6c59ce2b57442..321e55edf6eb4 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@vscode/codicons": "^0.0.46-14", "@vscode/copilot-api": "^0.4.1", "@vscode/deviceid": "^0.1.1", - "@vscode/diff": "^0.0.2-0", + "@vscode/diff": "0.0.2-7", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", "@vscode/policy-watcher": "^1.3.2", From 1fb463b5e2d0518fc85d244f1ae8c5c3f9be8b59 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 28 May 2026 16:56:58 +0100 Subject: [PATCH 7/8] Improve session list UI consistency (#318752) * fix: update font sizes and weights in session styles for consistency Co-authored-by: Copilot * fix: update codicon sizes to use compact variants for improved UI consistency --------- Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../sessions/browser/media/sessionsList.css | 20 +++++++++---------- .../sessions/browser/views/sessionsList.ts | 12 +++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css index 6e350617ea492..33edc6bdc137c 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css @@ -113,7 +113,7 @@ > .codicon { flex-shrink: 0; - font-size: 12px; + font-size: var(--vscode-codiconFontSize-compact, 12px); height: 16px; display: flex; align-items: center; @@ -154,7 +154,7 @@ .session-details-row { gap: 4px; - font-size: 12px; + font-size: var(--vscode-agents-fontSize-label3, 11px); line-height: 15px; max-height: 15px; overflow: hidden; @@ -165,7 +165,7 @@ align-items: center; > .codicon { - font-size: 12px; + font-size: var(--vscode-codiconFontSize-compact, 12px); } } @@ -224,7 +224,7 @@ } .session-title { - font-size: 13px; + font-size: var(--vscode-agents-fontSize-body1, 13px); } .session-title { @@ -272,7 +272,7 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - font-size: 12px; + font-size: var(--vscode-agents-fontSize-label1, 12px); } } @@ -281,7 +281,7 @@ .monaco-button { padding: 2px 10px; - font-size: 12px; + font-size: var(--vscode-agents-fontSize-label1, 12px); white-space: nowrap; } } @@ -295,7 +295,7 @@ justify-content: center; align-items: center; padding: 0 10px; - font-size: 11px; + font-size: var(--vscode-agents-fontSize-label3, 11px); color: var(--vscode-descriptionForeground); min-height: 26px; cursor: pointer; @@ -322,7 +322,7 @@ /* Folders show-more/show-less: match the folder section header style */ .session-show-more.session-show-more-folders { - font-weight: 500; + font-weight: var(--vscode-agents-fontWeight-medium, 500); text-transform: uppercase; /* No surrounding separator lines for the folders show-more/less item */ @@ -337,8 +337,8 @@ .session-section { display: flex; align-items: center; - font-size: 11px; - font-weight: 500; + font-size: var(--vscode-agents-fontSize-label2, 11px); + font-weight: var(--vscode-agents-fontWeight-medium, 500); color: var(--vscode-descriptionForeground); text-transform: uppercase; /* align left with session item margin; keep extra right padding for section content spacing */ diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index abe02e68313df..821d384fda840 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -454,7 +454,7 @@ class SessionItemRenderer implements ITreeRenderer 0 && workspace?.folders[0]?.gitRepository?.workTreeUri === undefined; - const icon = workspace?.isVirtualWorkspace ? Codicon.cloud : isWorkspaceSession ? Codicon.folder : Codicon.worktree; + const icon = workspace?.isVirtualWorkspace ? Codicon.cloudCompact : isWorkspaceSession ? Codicon.folderCompact : Codicon.worktreeCompact; const typeIconEl = DOM.append(template.detailsRow, $('span.session-details-icon')); DOM.append(typeIconEl, $(`span${ThemeIcon.asCSSSelector(icon)}`)); parts.push(typeIconEl); @@ -631,20 +631,20 @@ class SessionItemRenderer implements ITreeRenderer Date: Thu, 28 May 2026 12:20:09 -0400 Subject: [PATCH 8/8] Fix credit wrapping (#318760) * Fix credit wrapping * Fix UI --- .../browser/chatStatus/chatStatusDashboard.ts | 18 ++++++++---------- .../browser/chatStatus/media/chatStatus.css | 4 ++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 8b87991475a0f..21fa76d52e15d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -108,7 +108,7 @@ export class ChatStatusDashboard extends DomWidget { private readonly timeFormatter = safeIntl.DateTimeFormat(language, { hour: 'numeric', minute: 'numeric' }); private readonly quotaPercentageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 0, minimumFractionDigits: 0 }); private readonly quotaCreditsFormatter = safeIntl.NumberFormat(language, { maximumFractionDigits: 2, minimumFractionDigits: 0 }); - private readonly quotaCreditsCompactFormatter = safeIntl.NumberFormat(language, { notation: 'compact', maximumFractionDigits: 1, minimumFractionDigits: 0 }); + constructor( private readonly options: IChatStatusDashboardOptions | undefined, @@ -726,10 +726,13 @@ export class ChatStatusDashboard extends DomWidget { quotaPercentage.tabIndex = isCompact ? -1 : 0; const indicatorElement = $('div.quota-indicator', undefined, - $('div.quota-title', undefined, isCompact ? compactTitle : label), + $('div.quota-title', undefined, + $('span', undefined, isCompact ? compactTitle : label), + ...isCompact ? [] : [resetValue] + ), $('div.quota-details', undefined, quotaPercentage, - resetValue + ...isCompact ? [resetValue] : [] ), ...isCompact ? [] : [$('div.quota-bar', undefined, quotaBit)] ); @@ -760,13 +763,8 @@ export class ChatStatusDashboard extends DomWidget { const used = currentQuota.quotaRemaining !== undefined ? total - currentQuota.quotaRemaining : total * (100 - currentQuota.percentRemaining) / 100; - const compactThreshold = 100_000; - const usedFormatted = used >= compactThreshold - ? this.quotaCreditsCompactFormatter.value.format(used) - : this.quotaCreditsFormatter.value.format(used); - const totalFormatted = total >= compactThreshold - ? this.quotaCreditsCompactFormatter.value.format(total) - : this.quotaCreditsFormatter.value.format(total); + const usedFormatted = this.quotaCreditsFormatter.value.format(used); + const totalFormatted = this.quotaCreditsFormatter.value.format(total); quotaValueText.textContent = localize('quotaCreditsDisplay', "{0} / {1}", usedFormatted, totalFormatted); quotaValueSuffix.textContent = isCompact ? localize('quotaLabelUsed', "{0} used", label) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css index 660be196176cb..513b5e3db7a8c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css @@ -216,6 +216,9 @@ } .chat-status-bar-entry-tooltip .quota-indicator .quota-title { + display: flex; + justify-content: space-between; + align-items: baseline; font-size: 13px; line-height: 18px; font-weight: 600; @@ -252,6 +255,7 @@ .chat-status-bar-entry-tooltip .quota-indicator .quota-reset { font-size: 12px; + font-weight: normal; line-height: 16px; color: var(--vscode-descriptionForeground); white-space: nowrap;