diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index 0a6e82b2a54af..5d1beb9488986 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -5,10 +5,10 @@ "resolved": "ghcr.io/devcontainers/features/desktop-lite@sha256:14ac23fd59afab939e6562ba6a1f42a659a805e4c574a1be23b06f28eb3b0b71", "integrity": "sha256:14ac23fd59afab939e6562ba6a1f42a659a805e4c574a1be23b06f28eb3b0b71" }, - "ghcr.io/devcontainers/features/rust:1": { - "version": "1.3.3", - "resolved": "ghcr.io/devcontainers/features/rust@sha256:2521a8eeb4911bfcb22557c8394870ea22eb790d8e52219ddc5182f62d388995", - "integrity": "sha256:2521a8eeb4911bfcb22557c8394870ea22eb790d8e52219ddc5182f62d388995" + "ghcr.io/devcontainers/features/rust:": { + "version": "1.5.0", + "resolved": "ghcr.io/devcontainers/features/rust@sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c", + "integrity": "sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c" } } } \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 195d5a4b18f68..947527eb4c90f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ }, "features": { "ghcr.io/devcontainers/features/desktop-lite:": {}, - "ghcr.io/devcontainers/features/rust:1": {} + "ghcr.io/devcontainers/features/rust:": {} }, "containerEnv": { "DISPLAY": "" // Allow the Dev Containers extension to set DISPLAY, post-create.sh will add it back in ~/.bashrc and ~/.zshrc if not set. diff --git a/.github/instructions/accessibility.instructions.md b/.github/instructions/accessibility.instructions.md index 7777574315a5d..d6e800992fadf 100644 --- a/.github/instructions/accessibility.instructions.md +++ b/.github/instructions/accessibility.instructions.md @@ -1,273 +1,79 @@ --- -description: Accessibility guidelines for VS Code features — covers accessibility help dialogs, accessible views, verbosity settings, accessibility signals, ARIA alerts/status announcements, keyboard navigation, and ARIA labels/roles. Applies to both new interactive UI surfaces and updates to existing features. +description: 'Use when implementing accessibility features, ARIA labels, screen reader support, accessible help dialogs, or keybinding scoping for accessibility. Covers AccessibleContentProvider, CONTEXT_ACCESSIBILITY_MODE_ENABLED, verbosity settings, and announcement patterns.' --- -When adding a **new interactive UI surface** to VS Code — a panel, view, widget, editor overlay, dialog, or any rich focusable component the user interacts with — you **must** provide three accessibility components (if they do not already exist for the feature): +# Accessibility Guidelines -1. **An Accessibility Help Dialog** — opened via the accessibility help keybinding when the feature has focus. -2. **An Accessible View** — a plain-text read-only editor that presents the feature's content to screen reader users (when the feature displays non-trivial visual content). -3. **An Accessibility Verbosity Setting** — a boolean setting that controls whether the "open accessibility help" hint is announced. +Accessibility is a high-priority area in VS Code. Follow these patterns to ensure features work correctly with screen readers and assistive technologies. -Examples of existing features that have all three: the **terminal**, **chat panel**, **notebook**, **diff editor**, **inline completions**, **comments**, **debug REPL**, **hover**, and **notifications**. Features with only a help dialog (no accessible view) include **find widgets**, **source control input**, **keybindings editor**, **problems panel**, and **walkthroughs**. +## Keybinding Scoping -Sections 4–7 below (signals, ARIA announcements, keyboard navigation, ARIA labels) apply more broadly to **any UI change**, including modifications to existing features. +Accessibility-specific keybindings MUST be scoped to prevent conflicts with standard shortcuts: -When **updating an existing feature** — for example, adding new commands, keyboard shortcuts, or interactive capabilities — you must also update the feature's existing accessibility help dialog (`provideContent()`) to document the new functionality. Screen reader users rely on the help dialog as the primary way to discover available actions. - ---- - -## 1. Accessibility Help Dialog - -An accessibility help dialog tells the user what the feature does, which keyboard shortcuts are available, and how to interact with it via a screen reader. - -### Steps - -1. **Create a class implementing `IAccessibleViewImplementation`** with `type = AccessibleViewType.Help`. - - Set a `priority` (higher = shown first when multiple providers match). - - Set `when` to a `ContextKeyExpression` that matches when the feature is focused. - - `getProvider(accessor)` returns an `AccessibleContentProvider`. - -2. **Create a content-provider class** implementing `IAccessibleViewContentProvider`. - - `id` — add a new entry in the `AccessibleViewProviderId` enum in `src/vs/platform/accessibility/browser/accessibleView.ts`. - - `verbositySettingKey` — reference the new `AccessibilityVerbositySettingId` entry (see §3). - - `options` — `{ type: AccessibleViewType.Help }`. - - `provideContent()` — return localized, multi-line help text. - -3. **Implement `onClose()`** to restore focus to whatever element was focused before the help dialog opened. This ensures keyboard users and screen reader users return to their previous context. - -4. **Register** the implementation: - ```ts - AccessibleViewRegistry.register(new MyFeatureAccessibilityHelp()); - ``` - in the feature's `*.contribution.ts` file. - -### Example skeleton - -```ts -import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId, IAccessibleViewContentProvider, IAccessibleViewOptions } from '…/accessibleView.js'; -import { IAccessibleViewImplementation } from '…/accessibleViewRegistry.js'; -import { AccessibilityVerbositySettingId } from '…/accessibilityConfiguration.js'; +```typescript +keybinding: { + primary: KeyCode.F7, + when: ContextKeyExpr.and( + EditorContextKeys.focus, + CONTEXT_ACCESSIBILITY_MODE_ENABLED + ), + weight: KeybindingWeight.WorkbenchContrib, +}, +``` -export class MyFeatureAccessibilityHelp implements IAccessibleViewImplementation { - readonly priority = 100; - readonly name = 'my-feature'; - readonly type = AccessibleViewType.Help; - readonly when = MyFeatureContextKeys.isFocused; +**Why**: Without scoping, accessibility shortcuts steal commonly used keybindings (e.g., F7 for "Go to Next Symbol Highlight"). PR #293163 fixed this exact conflict. - getProvider(accessor: ServicesAccessor) { - return new MyFeatureAccessibilityHelpProvider(); - } -} +## Accessible Help Dialogs (Alt+F1) -class MyFeatureAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider { - readonly id = AccessibleViewProviderId.MyFeature; - readonly verbositySettingKey = AccessibilityVerbositySettingId.MyFeature; - readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; +Use `AccessibleContentProvider` directly — do not create custom subclasses: - provideContent(): string { - return [ - localize('myFeature.help.header', "Accessibility Help: My Feature"), - localize('myFeature.help.overview', "You are in My Feature. …"), - '', - localize('myFeature.help.keys', "Keyboard shortcuts:"), - localize('myFeature.help.key1', "- {0}: Do something", ''), - ].join('\n'); - } +```typescript +getProvider(): AccessibleContentProvider { + return new AccessibleContentProvider( + AccessibleViewProviderId.MyFeature, + { type: AccessibleViewType.Help }, + () => this.getHelpContent(), + () => this.onClose(), + 'accessibility.verbosity.myFeature' + ); } ``` ---- - -## 2. Accessible View - -An accessible view presents the feature's visual content as plain text in a read-only editor. It is required when the feature renders rich or visual content that a screen reader cannot directly read (for example: chat responses, hover tooltips, notifications, terminal output, inline completions). +## Announcement Patterns -If the feature is purely keyboard-driven with native text input/output (e.g., a simple input field), an accessible view is not needed — only an accessibility help dialog is required. +When announcing state changes to screen readers: -### Steps +1. **Only announce when conditions are met**: Check that a search string is present, widget is visible, and screen reader is active +2. **Prevent double-speak**: Track announcement state with a flag and use a timeout (~1 second) +3. **Use verbosity settings**: Check `accessibility.verbosity.[feature]` before verbose announcements -1. **Create a class implementing `IAccessibleViewImplementation`** with `type = AccessibleViewType.View`. -2. **Create a content-provider** similar to the help dialog, but: - - `options` — `{ type: AccessibleViewType.View }`, optionally with a `language` for syntax highlighting. - - `provideContent()` — return the feature's current content as plain text. - - Optionally implement `provideNextContent()` / `providePreviousContent()` for item-by-item navigation. - - Implement `onClose()` to restore focus to whatever was focused before the accessible view was opened. - - Optionally provide `actions` for actions the user can take from the accessible view. -3. **Register** alongside the help dialog: - ```ts - AccessibleViewRegistry.register(new MyFeatureAccessibleView()); - ``` - -### Example skeleton - -```ts -export class MyFeatureAccessibleView implements IAccessibleViewImplementation { - readonly priority = 100; - readonly name = 'my-feature'; - readonly type = AccessibleViewType.View; - readonly when = MyFeatureContextKeys.isFocused; - - getProvider(accessor: ServicesAccessor) { - // Retrieve services, build content from the feature's current state - const content = getMyFeatureContent(); - if (!content) { - return undefined; - } - return new AccessibleContentProvider( - AccessibleViewProviderId.MyFeature, - { type: AccessibleViewType.View }, - () => content, - () => { /* onClose — refocus whatever was focused before the accessible view opened */ }, - AccessibilityVerbositySettingId.MyFeature, - ); - } +```typescript +if (this.accessibilityService.isScreenReaderOptimized() && + this.configService.getValue('accessibility.verbosity.hover') && + !this._accessibilityHelpHintAnnounced) { + this._accessibilityHelpHintAnnounced = true; + // announce hint } ``` ---- - -## 3. Accessibility Verbosity Setting - -A verbosity setting controls whether a hint such as "press Alt+F1 for accessibility help" is announced when the feature gains focus. Users who already know the shortcut can disable it. - -### Steps - -1. **Add an entry** to `AccessibilityVerbositySettingId` in - `src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts`: - ```ts - export const enum AccessibilityVerbositySettingId { - // … existing entries … - MyFeature = 'accessibility.verbosity.myFeature' - } - ``` - -2. **Register the configuration property** in the same file's `configuration.properties` object: - ```ts - [AccessibilityVerbositySettingId.MyFeature]: { - description: localize('verbosity.myFeature.description', - 'Provide information about how to access the My Feature accessibility help menu when My Feature is focused.'), - ...baseVerbosityProperty - }, - ``` - The `baseVerbosityProperty` gives it `type: 'boolean'`, `default: true`, and `tags: ['accessibility']`. - -3. **Reference the setting key** in both the help-dialog provider (`verbositySettingKey`) and the accessible-view provider so the runtime can check whether to show the hint. - ---- - -## 4. Accessibility Signals (Sounds & Announcements) - -Accessibility signals provide audible and spoken feedback for events that happen visually. Use `IAccessibilitySignalService` to play signals when something important occurs (e.g., an error appears, a task completes, content changes). - -### When to use +## Multi-Modal Notifications -- **Use an existing signal** when the event already has one defined (see `AccessibilitySignal.*` static members — e.g., `AccessibilitySignal.errorAtPosition`, `AccessibilitySignal.terminalQuickFix`, `AccessibilitySignal.clear`). -- **If no existing signal fits**, reach out to @meganrogge to discuss adding a new one. Do not register new signals without coordinating first. +For important state changes, provide all three: +1. **Audio signal**: `this.accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired)` +2. **ARIA alert**: `status(message)` for screen readers +3. **OS notification**: Respect `chat.notifyWindowOnConfirmation` setting. OS notifications are **conditional on window focus** — only fire when `targetWindow.document.hasFocus()` is `false` (see `ChatWindowNotifier`) -### How signals work +## Configuration -Each signal has two modalities controlled by user settings: -- **Sound** — a short audio cue, configurable to `auto` (on when screen reader attached), `on`, or `off`. -- **Announcement** — a spoken message via `aria-live`, configurable to `auto` or `off`. +When adding a new accessible feature, register a verbosity setting: -### Usage - -```ts -// Inject the service via constructor parameter -constructor( - @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService -) { } - -// Play a signal -this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalQuickFix); - -// Play with options -this._accessibilitySignalService.playSignal(AccessibilitySignal.error, { userGesture: true }); +```typescript +// In accessibility configuration +'accessibility.verbosity.myFeature': { + type: 'boolean', + default: true, + description: localize('accessibility.verbosity.myFeature', "...") +} ``` ---- - -## 5. ARIA Alerts vs. Status Messages - -Use the `alert()` and `status()` functions from `src/vs/base/browser/ui/aria/aria.ts` to announce dynamic changes to screen readers. - -### `alert(msg)` — Assertive live region (`role="alert"`) -- **Use for**: Urgent, important information that the user must know immediately. -- **Examples**: Errors, warnings, critical state changes, results of a user-initiated action. -- **Behavior**: Interrupts the screen reader's current speech. - -### `status(msg)` — Polite live region (`aria-live="polite"`) -- **Use for**: Non-urgent, informational updates that should be spoken when the screen reader is idle. -- **Examples**: Progress updates, search result counts, background state changes. -- **Behavior**: Queued and spoken after the screen reader finishes its current output. - -### Guidelines - -- **Prefer `status()` over `alert()`** unless the information is time-sensitive or the result of a direct user action. Overusing `alert()` creates a noisy, disruptive experience. -- **Keep messages concise.** Screen readers read the entire message; long messages delay the user. -- **Do not duplicate** — if an accessibility signal already announces the event, do not also call `alert()` / `status()` for the same information. -- **Localize** all messages with `nls.localize()`. - ---- - -## 6. Keyboard Navigation - -Every interactive UI element must be fully operable via the keyboard. - -### Requirements - -- **Tab order**: All interactive elements must be reachable via `Tab` / `Shift+Tab` in a logical order. -- **Arrow key navigation**: Lists, trees, grids, and toolbars must support arrow key navigation following WAI-ARIA patterns. -- **Focus visibility**: Focused elements must have a visible focus indicator (VS Code's theme system provides this via `focusBorder`). -- **No mouse-only interactions**: Every action reachable by click or hover must also be reachable via keyboard (context menus, buttons, toggles, etc.). -- **Escape to dismiss**: Overlays, dialogs, and popups must be dismissable with `Escape`, returning focus to the previous element. -- **Focus trapping**: Modal dialogs must trap focus within the dialog until dismissed. - ---- - -## 7. ARIA Labels and Roles - -All interactive UI elements must have appropriate ARIA attributes so screen readers can identify and describe them. - -### Requirements - -- **`aria-label`**: Every interactive element without visible text (icon buttons, icon-only actions, custom widgets) must have a descriptive `aria-label`. Labels should be localized. -- **`aria-labelledby`** / **`aria-describedby`**: Use these to associate elements with existing visible text rather than duplicating strings. -- **`role`**: Custom widgets that do not use native HTML elements must declare the correct ARIA role (e.g., `role="button"`, `role="tree"`, `role="tablist"`). -- **`aria-expanded`**, **`aria-selected`**, **`aria-checked`**: Toggle and selection states must be communicated via the appropriate ARIA state attributes. -- **`aria-hidden="true"`**: Decorative or redundant elements (icons next to text labels, decorative separators) must be hidden from the accessibility tree. - -### Guidelines - -- Avoid generic labels like "button" or "icon" — describe the action: "Close panel", "Toggle sidebar", "Run task". -- Test with a screen reader (VoiceOver on macOS, NVDA on Windows) to verify labels are spoken correctly in context. -- Lists and trees should use `aria-setsize` and `aria-posinset` when virtualized so screen readers report the correct count. - ---- - -## Checklist for Every New Feature - -- [ ] New `AccessibleViewProviderId` entry added in `accessibleView.ts` -- [ ] New `AccessibilityVerbositySettingId` entry added in `accessibilityConfiguration.ts` -- [ ] Verbosity setting registered in the configuration properties with `...baseVerbosityProperty` -- [ ] `IAccessibleViewImplementation` with `type = Help` created and registered -- [ ] Content provider references the correct `verbositySettingKey` -- [ ] Help text is fully localized using `nls.localize()` -- [ ] Keybindings in help text use `` syntax for dynamic resolution -- [ ] `when` context key is set so the dialog only appears when the feature is focused -- [ ] If the feature has rich/visual content: `IAccessibleViewImplementation` with `type = View` created and registered -- [ ] Registration calls in the feature's `*.contribution.ts` file -- [ ] Accessibility signal played for important events (use existing `AccessibilitySignal.*` or register a new one) -- [ ] `aria.alert()` or `aria.status()` used appropriately for dynamic changes (prefer `status()` unless urgent) -- [ ] All interactive elements reachable and operable via keyboard -- [ ] All interactive elements without visible text have a localized `aria-label` -- [ ] Custom widgets declare the correct ARIA `role` and state attributes -- [ ] Decorative elements are hidden with `aria-hidden="true"` - -## Key Files - -- `src/vs/platform/accessibility/browser/accessibleView.ts` — `AccessibleViewProviderId`, `AccessibleContentProvider`, `IAccessibleViewContentProvider` -- `src/vs/platform/accessibility/browser/accessibleViewRegistry.ts` — `AccessibleViewRegistry`, `IAccessibleViewImplementation` -- `src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts` — `AccessibilityVerbositySettingId`, verbosity setting registration -- `src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts` — `IAccessibilitySignalService`, `AccessibilitySignal` -- `src/vs/base/browser/ui/aria/aria.ts` — `alert()`, `status()` for ARIA live region announcements +And extend `AccessibleViewProviderId` and `AccessibilityVerbositySettingId` enums for new help providers. diff --git a/.npmrc b/.npmrc index 50d910c65e4ec..abbc265436f6c 100644 --- a/.npmrc +++ b/.npmrc @@ -2,6 +2,7 @@ disturl="https://electronjs.org/headers" target="39.3.0" ms_build_id="13168319" runtime="electron" +ignore-scripts=false build_from_source="true" legacy-peer-deps="true" timeout=180000 diff --git a/build/package-lock.json b/build/package-lock.json index 255effe830ecf..0a99309c06321 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -53,7 +53,7 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "dmg-builder": "^26.5.0", + "dmg-builder": "^26.7.0", "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", @@ -813,14 +813,13 @@ } }, "node_modules/@electron/rebuild": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.1.tgz", - "integrity": "sha512-iMGXb6Ib7H/Q3v+BKZJoETgF9g6KMNZVbsO4b7Dmpgb5qTFqyFTzqW9F3TOSHdybv2vKYKzSS9OiZL+dcJb+1Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", "dev": true, "license": "MIT", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", @@ -831,7 +830,7 @@ "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", - "tar": "^6.0.5", + "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { @@ -841,142 +840,6 @@ "node": ">=22.12.0" } }, - "node_modules/@electron/rebuild/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@electron/rebuild/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@electron/rebuild/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/rebuild/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@electron/rebuild/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/rebuild/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@electron/rebuild/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@electron/rebuild/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@electron/rebuild/node_modules/node-abi": { "version": "4.26.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", @@ -991,9 +854,9 @@ } }, "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -1003,38 +866,6 @@ "node": ">=10" } }, - "node_modules/@electron/rebuild/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@electron/universal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", @@ -1571,9 +1402,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1792,9 +1623,9 @@ } }, "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3136,18 +2967,19 @@ "license": "MIT" }, "node_modules/app-builder-lib": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.5.0.tgz", - "integrity": "sha512-iRRiJhM0uFMauDeIuv8ESHZSn+LESbdDEuHi7rKdeETjrvBObecXnWJx1f3vs3KtoGcd3hCk1zURKypyvZOtFQ==", + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.7.0.tgz", + "integrity": "sha512-/UgCD8VrO79Wv8aBNpjMfsS1pIUfIPURoRn0Ik6tMe5avdZF+vQgl/juJgipcMmH3YS0BD573lCdCHyoi84USg==", "dev": true, "license": "MIT", "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "4.0.1", + "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", @@ -3160,7 +2992,7 @@ "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", - "electron-publish": "26.4.1", + "electron-publish": "26.6.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", @@ -3170,9 +3002,10 @@ "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", + "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", - "tar": "7.5.3", + "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" @@ -3181,8 +3014,55 @@ "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "26.5.0", - "electron-builder-squirrel-windows": "26.5.0" + "dmg-builder": "26.7.0", + "electron-builder-squirrel-windows": "26.7.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/app-builder-lib/node_modules/@electron/osx-sign": { @@ -3242,14 +3122,37 @@ "node": ">=12" } }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/app-builder-lib/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.2.tgz", + "integrity": "sha512-mIcis6w+JiQf3P7t7mg/35GKB4T1FQsBOtMIvuKw4YErj5RjtbhcTd5/I30fmkmGMwvI0WlzSNN+27K0QCMkAw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/app-builder-lib/node_modules/js-yaml": { @@ -3265,27 +3168,14 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { "node": "20 || >=22" @@ -3295,9 +3185,9 @@ } }, "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3307,16 +3197,6 @@ "node": ">=10" } }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -3398,6 +3278,13 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -3822,6 +3709,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -4595,13 +4483,13 @@ } }, "node_modules/dmg-builder": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.5.0.tgz", - "integrity": "sha512-AyOCzpS1TCxDkSWxAzpfw5l7jBX4C8jKCucmT/6y6/24H5VKSHpjcVJD0W8o5BrFi+skC7Z7+F4aNyHmvn4AAw==", + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.7.0.tgz", + "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.5.0", + "app-builder-lib": "26.7.0", "builder-util": "26.4.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", @@ -4883,9 +4771,9 @@ } }, "node_modules/electron-publish": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.4.1.tgz", - "integrity": "sha512-nByal9K5Ar3BNJUfCSglXltpKUhJqpwivNpKVHnkwxTET9LKl+NxoojpGF1dSXVFcoBKVm+OhsVa28ZsoshEPA==", + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", + "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4893,7 +4781,7 @@ "builder-util": "26.4.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", - "form-data": "^4.0.0", + "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" @@ -5513,9 +5401,9 @@ "dev": true }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -6332,13 +6220,6 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -7011,19 +6892,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -7114,9 +6982,9 @@ } }, "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -7164,19 +7032,19 @@ } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.2.tgz", + "integrity": "sha512-mIcis6w+JiQf3P7t7mg/35GKB4T1FQsBOtMIvuKw4YErj5RjtbhcTd5/I30fmkmGMwvI0WlzSNN+27K0QCMkAw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -7907,6 +7775,25 @@ "node": ">=10" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -9054,10 +8941,9 @@ } }, "node_modules/tar": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz", - "integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/build/package.json b/build/package.json index e45161dc2c328..b20e60c31dd5e 100644 --- a/build/package.json +++ b/build/package.json @@ -47,7 +47,7 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "dmg-builder": "^26.5.0", + "dmg-builder": "^26.7.0", "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", diff --git a/build/win32/code.iss b/build/win32/code.iss index e473aa1e761cc..d889ca8c4d383 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -110,9 +110,9 @@ Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourc #endif [Icons] -Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" -Name: "{autodesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" -Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}" +Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{group}\{#NameLong}.lnk')) +Name: "{autodesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{autodesktop}\{#NameLong}.lnk')) +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}.lnk')) [Run] Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate @@ -1448,6 +1448,11 @@ begin Result := FileExists(ExpandConstant('{param:sessionend}')) end; +function ShouldUpdateShortcut(Path: String): Boolean; +begin + Result := not (IsBackgroundUpdate() and FileExists(Path)); +end; + function ShouldRunAfterUpdate(): Boolean; begin if IsBackgroundUpdate() then diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 992e23a44100c..5209f73ad91a6 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -331,9 +331,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f6e2f96dbd476..1c1f8d93b57c8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -52,7 +52,7 @@ thiserror = "1.0.40" cfg-if = "1.0.0" pin-project = "1.1.0" console = "0.15.7" -bytes = "1.4.0" +bytes = "1.11.1" tar = "0.4.38" [build-dependencies] diff --git a/eslint.config.js b/eslint.config.js index 4118fd0759a66..2a3cec2b0e535 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -564,7 +564,6 @@ export default tseslint.config( 'src/vs/workbench/api/common/extHostWebviewView.ts', 'src/vs/workbench/api/common/extHostWorkspace.ts', 'src/vs/workbench/api/common/extensionHostMain.ts', - 'src/vs/workbench/api/common/shared/tasks.ts', 'src/vs/workbench/api/node/extHostAuthentication.ts', 'src/vs/workbench/api/node/extHostCLIServer.ts', 'src/vs/workbench/api/node/extHostConsoleForwarder.ts', @@ -700,16 +699,6 @@ export default tseslint.config( 'src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts', 'src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts', 'src/vs/workbench/contrib/snippets/browser/snippetsService.ts', - 'src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts', - 'src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts', - 'src/vs/workbench/contrib/tasks/browser/task.contribution.ts', - 'src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts', - 'src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts', - 'src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts', - 'src/vs/workbench/contrib/tasks/common/problemMatcher.ts', - 'src/vs/workbench/contrib/tasks/common/taskConfiguration.ts', - 'src/vs/workbench/contrib/tasks/common/taskSystem.ts', - 'src/vs/workbench/contrib/tasks/common/tasks.ts', 'src/vs/workbench/contrib/testing/common/storedValue.ts', 'src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts', 'src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts', diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index ff4be15a69482..c946703eea8e7 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -1,6 +1,7 @@ { "$schema": "vscode://schemas/color-theme", "name": "2026 Dark", + "include": "../../theme-defaults/themes/dark_modern.json", "type": "dark", "colors": { "foreground": "#bfbfbf", @@ -16,10 +17,11 @@ "textLink.activeForeground": "#53A5CA", "textPreformat.foreground": "#888888", "textSeparator.foreground": "#2a2a2aFF", - "button.background": "#3994BC", + "button.background": "#3994BCF2", "button.foreground": "#FFFFFF", "button.hoverBackground": "#3E9BC4", - "button.border": "#2A2B2CFF", + "button.border": "#333536FF", + "button.secondaryHoverBackground": "#FFFFFF10", "checkbox.background": "#242526", "checkbox.border": "#333536", "checkbox.foreground": "#bfbfbf", @@ -47,7 +49,7 @@ "scrollbarSlider.background": "#83848533", "scrollbarSlider.hoverBackground": "#83848566", "scrollbarSlider.activeBackground": "#83848599", - "badge.background": "#3994BC", + "badge.background": "#3994BCF0", "badge.foreground": "#FFFFFF", "progressBar.background": "#878889", "list.activeSelectionBackground": "#3994BC55", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index c4f66b51a9053..37e3e68133414 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -1,6 +1,7 @@ { "$schema": "vscode://schemas/color-theme", "name": "2026 Light", + "include": "../../theme-defaults/themes/light_modern.json", "type": "light", "colors": { "foreground": "#202020", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 16005a52409c7..1401d0551a6fe 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -26,15 +26,25 @@ --backdrop-blur-lg: blur(40px) saturate(180%); } +/* Dark theme: add brightness reduction for contrast-safe luminosity blending over bright backgrounds */ +.monaco-workbench.vs-dark { + --backdrop-blur-sm: blur(12px) brightness(0.55); + --backdrop-blur-md: blur(20px) saturate(180%) brightness(0.55); + --backdrop-blur-lg: blur(40px) saturate(180%) brightness(0.55); +} + /* Stealth Shadows - shadow-based depth for UI elements, controlled by workbench.stealthShadows.enabled */ /* Activity Bar */ .monaco-workbench .part.activitybar { - box-shadow: var(--shadow-md); z-index: 50; position: relative; } +.monaco-workbench.nosidebar .part.activitybar { + box-shadow: var(--shadow-md); +} + .monaco-workbench.activitybar-right .part.activitybar { box-shadow: var(--shadow-md); } @@ -52,7 +62,7 @@ .monaco-workbench .part.auxiliarybar { box-shadow: var(--shadow-md); - z-index: 40; + z-index: 35; position: relative; } @@ -75,7 +85,7 @@ /* Panel */ .monaco-workbench .part.panel { box-shadow: var(--shadow-md); - z-index: 35; + /* z-index: 35; */ position: relative; } @@ -93,15 +103,19 @@ /* Sashes - ensure they extend full height and are above other panels */ .monaco-workbench .monaco-sash { - z-index: 45; + z-index: 35; } .monaco-workbench .monaco-sash.vertical { + z-index: 40; +} + +.monaco-workbench .monaco-sash.vertical:nth-child(2) { z-index: 45; } .monaco-workbench .monaco-sash.horizontal { - z-index: 45; + z-index: 35; } /* Editor */ @@ -146,32 +160,14 @@ box-shadow: var(--shadow-md); } -.monaco-workbench .part.titlebar { - position: relative; - background: linear-gradient( - to bottom, - color-mix(in srgb, var(--vscode-focusBorder) 10%, transparent) 0%, - transparent 100% - ), var(--vscode-titleBar-activeBackground) !important; -} - .monaco-workbench .part.titlebar.inactive { background: var(--vscode-titleBar-inactiveBackground) !important; - .command-center .monaco-action-bar, - .command-center .actions-container, - .agent-status-pill, .agent-status-badge { - background-color: transparent !important; + & > * { + opacity: 0.3; } } -/* Status Bar */ -.monaco-workbench .part.statusbar { - box-shadow: var(--shadow-md); - z-index: 55; - position: relative; -} - /* Quick Input (Command Palette) */ .monaco-workbench .quick-input-widget { box-shadow: var(--shadow-xl) !important; @@ -179,7 +175,6 @@ background-color: color-mix(in srgb, var(--vscode-quickInput-background) 60%, transparent) !important; backdrop-filter: var(--backdrop-blur-lg); -webkit-backdrop-filter: var(--backdrop-blur-lg); - -webkit-backdrop-filter: blur(40px) saturate(180%); } .monaco-workbench.vs-dark .quick-input-widget { @@ -613,9 +608,9 @@ /* Notebook */ -.monaco-workbench .notebookOverlay.notebook-editor { +/* .monaco-workbench .notebookOverlay.notebook-editor { z-index: 35 !important; -} +} */ .monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { box-shadow: inset var(--shadow-sm); diff --git a/package-lock.json b/package-lock.json index e3366fe648c9c..49beda3da1a29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -156,7 +156,7 @@ "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", - "webpack": "^5.94.0", + "webpack": "^5.105.0", "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", @@ -1359,9 +1359,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", - "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { @@ -1370,9 +1370,9 @@ } }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -4747,6 +4747,16 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -4860,9 +4870,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -4880,10 +4890,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -5090,9 +5101,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", "dev": true, "funding": [ { @@ -5126,15 +5137,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/chalk/node_modules/supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -6603,9 +6605,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.158", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz", - "integrity": "sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true, "license": "ISC" }, @@ -6643,14 +6645,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -8272,7 +8274,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/glob-watcher": { "version": "5.0.5", @@ -10026,6 +10029,16 @@ "node": ">=0.10.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -11009,15 +11022,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-report/node_modules/supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -11101,16 +11105,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -11699,12 +11693,17 @@ } }, "node_modules/loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -12516,15 +12515,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -12886,9 +12876,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -15063,9 +15053,9 @@ "dev": true }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { @@ -15421,15 +15411,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "node_modules/sinon/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/sinon/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -16368,12 +16349,17 @@ } }, "node_modules/tapable": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", - "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -16477,14 +16463,14 @@ } }, "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -16496,9 +16482,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -17301,9 +17287,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -17654,10 +17640,11 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, + "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -17679,9 +17666,9 @@ "dev": true }, "node_modules/webpack": { - "version": "5.100.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.0.tgz", - "integrity": "sha512-H8yBSBTk+BqxrINJnnRzaxU94SVP2bjd7WmA+PfCphoIdDpeQMJ77pq9/4I7xjLq38cB1bNKfzYPZu8pB3zKtg==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", "dependencies": { @@ -17693,22 +17680,22 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.2", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { @@ -17847,15 +17834,6 @@ "webpack": "^5.21.2" } }, - "node_modules/webpack-stream/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/webpack-stream/node_modules/replace-ext": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", @@ -17897,6 +17875,13 @@ "node": ">= 0.10" } }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index ccd707811dafe..d7794f4e97993 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", - "webpack": "^5.94.0", + "webpack": "^5.105.0", "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts index 774fc8ab10ea4..f012c86746449 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts @@ -185,7 +185,8 @@ export class TextAreaInput extends Disposable { } this._register(Event.runAndSubscribe(this._accessibilityService.onDidChangeScreenReaderOptimized, () => { if (this._accessibilityService.isScreenReaderOptimized() && !this._asyncFocusGainWriteScreenReaderContent.value) { - this._asyncFocusGainWriteScreenReaderContent.value = this._register(new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0)); + // Don't use this._register() here - the MutableDisposable already handles cleanup + this._asyncFocusGainWriteScreenReaderContent.value = new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0); } else { this._asyncFocusGainWriteScreenReaderContent.clear(); } diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 9a544c6d28f89..ef370b2289619 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -65,6 +65,7 @@ import { RulersGpu } from './viewParts/rulersGpu/rulersGpu.js'; import { GpuMarkOverlay } from './viewParts/gpuMark/gpuMark.js'; import { AccessibilitySupport } from '../../platform/accessibility/common/accessibility.js'; import { Event, Emitter } from '../../base/common/event.js'; +import { IUserInteractionService } from '../../platform/userInteraction/browser/userInteractionService.js'; export interface IContentWidgetData { @@ -139,13 +140,14 @@ export class View extends ViewEventHandler { model: IViewModel, userInputEvents: ViewUserInputEvents, overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IUserInteractionService private readonly _userInteractionService: IUserInteractionService, ) { super(); this._ownerID = ownerID; this._widgetFocusTracker = this._register( - new CodeEditorWidgetFocusTracker(editorContainer, overflowWidgetsDomNode) + new CodeEditorWidgetFocusTracker(editorContainer, overflowWidgetsDomNode, this._userInteractionService) ); this._register(this._widgetFocusTracker.onChange(() => { this._context.viewModel.setHasWidgetFocus(this._widgetFocusTracker.hasFocus()); @@ -940,11 +942,11 @@ class CodeEditorWidgetFocusTracker extends Disposable { private _hadFocus: boolean | undefined = undefined; - constructor(domElement: HTMLElement, overflowWidgetsDomNode: HTMLElement | undefined) { + constructor(domElement: HTMLElement, overflowWidgetsDomNode: HTMLElement | undefined, userInteractionService: IUserInteractionService) { super(); this._hasDomElementFocus = false; - this._domFocusTracker = this._register(dom.trackFocus(domElement)); + this._domFocusTracker = this._register(userInteractionService.createDomFocusTracker(domElement)); this._overflowWidgetsDomNodeHasFocus = false; @@ -958,7 +960,7 @@ class CodeEditorWidgetFocusTracker extends Disposable { })); if (overflowWidgetsDomNode) { - this._overflowWidgetsDomNode = this._register(dom.trackFocus(overflowWidgetsDomNode)); + this._overflowWidgetsDomNode = this._register(userInteractionService.createDomFocusTracker(overflowWidgetsDomNode)); this._register(this._overflowWidgetsDomNode.onDidFocus(() => { this._overflowWidgetsDomNodeHasFocus = true; this._update(); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 174eea21c6ab5..29d8394571114 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -64,6 +64,7 @@ import { MenuId } from '../../../../platform/actions/common/actions.js'; import { TextModelEditSource, EditSources } from '../../../common/textModelEditSource.js'; import { TextEdit } from '../../../common/core/edits/textEdit.js'; import { isObject } from '../../../../base/common/types.js'; +import { IUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js'; export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeEditor { @@ -249,6 +250,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE protected readonly _codeEditorService: ICodeEditorService; private readonly _commandService: ICommandService; private readonly _themeService: IThemeService; + private readonly _userInteractionService: IUserInteractionService; private _contentWidgets: { [key: string]: IContentWidgetData }; private _overlayWidgets: { [key: string]: IOverlayWidgetData }; @@ -279,6 +281,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE @IAccessibilityService accessibilityService: IAccessibilityService, @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IUserInteractionService userInteractionService: IUserInteractionService, ) { super(); codeEditorService.willCreateCodeEditor(); @@ -286,6 +289,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const options = { ..._options }; this._domElement = domElement; + this._userInteractionService = userInteractionService; this._overflowWidgetsDomNode = options.overflowWidgetsDomNode; delete options.overflowWidgetsDomNode; this._id = (++EDITOR_ID); @@ -1994,7 +1998,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE viewModel, viewUserInputEvents, this._overflowWidgetsDomNode, - this._instantiationService + this._instantiationService, + this._userInteractionService, ); return [view, true]; diff --git a/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts index 3852374d39496..da6c2b262f95f 100644 --- a/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts @@ -16,6 +16,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js'; export class EmbeddedCodeEditorWidget extends CodeEditorWidget { private readonly _parentEditor: ICodeEditor; @@ -34,9 +35,10 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget { @INotificationService notificationService: INotificationService, @IAccessibilityService accessibilityService: IAccessibilityService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IUserInteractionService userInteractionService: IUserInteractionService, ) { - super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); + super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService, userInteractionService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index 0c0f5a97e8d51..3929438266304 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ModifierKeyEmitter, n, trackFocus } from '../../../../../../../base/browser/dom.js'; +import { n } from '../../../../../../../base/browser/dom.js'; import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; import { BugIndicatingError } from '../../../../../../../base/common/errors.js'; @@ -35,6 +35,7 @@ import { InlineCompletionsModel } from '../../../model/inlineCompletionsModel.js import { InlineSuggestAlternativeAction } from '../../../model/InlineSuggestAlternativeAction.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ThemeIcon } from '../../../../../../../base/common/themables.js'; +import { IUserInteractionService } from '../../../../../../../platform/userInteraction/browser/userInteractionService.js'; /** * Customization options for the gutter indicator appearance and behavior. @@ -107,7 +108,8 @@ export class InlineEditsGutterIndicator extends Disposable { @IHoverService protected readonly _hoverService: HoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IThemeService private readonly _themeService: IThemeService + @IThemeService private readonly _themeService: IThemeService, + @IUserInteractionService private readonly _userInteractionService: IUserInteractionService ) { super(); @@ -162,7 +164,9 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _isHoveredOverInlineEditDebounced: IObservable; - private readonly _modifierPressed = observableFromEvent(this, ModifierKeyEmitter.getInstance().event, () => ModifierKeyEmitter.getInstance().keyStatus.shiftKey); + private readonly _modifierPressed = derived(this, reader => + this._userInteractionService.readModifierKeyStatus(this._editorObs.editor.getDomNode()!, reader).shiftKey + ); private readonly _gutterIndicatorStyles = derived(this, reader => { let v = this._tabAction.read(reader); @@ -476,9 +480,10 @@ export class InlineEditsGutterIndicator extends Disposable { }, ).toDisposableLiveElement()); - const focusTracker = disposableStore.add(trackFocus(content.element)); // TODO@benibenj should this be removed? - disposableStore.add(focusTracker.onDidBlur(() => this._focusIsInMenu.set(false, undefined))); - disposableStore.add(focusTracker.onDidFocus(() => this._focusIsInMenu.set(true, undefined))); + const isFocused = this._userInteractionService.createFocusTracker(content.element, disposableStore); // TODO@benibenj should this be removed? + disposableStore.add(autorun(reader => { + this._focusIsInMenu.set(isFocused.read(reader), undefined); + })); disposableStore.add(toDisposable(() => this._focusIsInMenu.set(false, undefined))); const h = this._hoverService.showInstantHover({ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts index c6e90caab9ab4..d6e259987ba47 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts @@ -10,6 +10,7 @@ import { IObservable, IReader, autorun, constObservable, derived, derivedObserva import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; +import { IUserInteractionService } from '../../../../../../../platform/userInteraction/browser/userInteractionService.js'; import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { Rect } from '../../../../../../common/core/2d/rect.js'; @@ -71,6 +72,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit private readonly _tabAction: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IThemeService private readonly _themeService: IThemeService, + @IUserInteractionService private readonly _userInteractionService: IUserInteractionService, ) { super(); this._editorObs = observableCodeEditor(this._editor); @@ -89,7 +91,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit }, [ n.div({ class: 'preview', style: { pointerEvents: 'none' }, ref: this.previewRef }), ]).keepUpdated(this._store); - this.isHovered = this._editorContainer.didMouseMoveDuringHover; + this.isHovered = this._userInteractionService.createHoverTracker(this._editorContainer.element, this._store); this.previewEditor = this._register(this._instantiationService.createInstance( EmbeddedCodeEditorWidget, this.previewRef.element, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts index cf4052733d9ee..b480191f79d0e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, ModifierKeyEmitter, n, ObserverNodeWithElement } from '../../../../../../../base/browser/dom.js'; +import { $, n, ObserverNodeWithElement } from '../../../../../../../base/browser/dom.js'; import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IEquatable } from '../../../../../../../base/common/equals.js'; @@ -34,6 +34,7 @@ import { InlineCompletionEditorType } from '../../../model/provideInlineCompleti import { IInlineEditsView, InlineEditClickEvent, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getEditorBackgroundColor, getModifiedBorderColor, getOriginalBorderColor, INLINE_EDITS_BORDER_RADIUS, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryBorder, inlineEditIndicatorPrimaryForeground, modifiedChangedTextOverlayColor, observeColor, originalChangedTextOverlayColor } from '../theme.js'; import { getEditorValidOverlayRect, mapOutFalsy, rectToProps } from '../utils/utils.js'; +import { IUserInteractionService } from '../../../../../../../platform/userInteraction/browser/userInteractionService.js'; export class WordReplacementsViewData implements IEquatable { constructor( @@ -80,6 +81,7 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin @IThemeService private readonly _themeService: IThemeService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IHoverService private readonly _hoverService: IHoverService, + @IUserInteractionService private readonly _userInteractionService: IUserInteractionService, ) { super(); this._start = this._editor.observePosition(constObservable(this._viewData.edit.range.getStartPosition()), this._store); @@ -87,7 +89,11 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin this._line = document.createElement('div'); this._primaryElement = observableValue(this, null); this._secondaryElement = observableValue(this, null); - this.isHovered = this._primaryElement.map((e, reader) => e?.didMouseMoveDuringHover.read(reader) ?? false); + this.isHovered = derived(this, reader => { + const elem = this._primaryElement.read(reader); + if (!elem) { return false; } + return this._userInteractionService.createHoverTracker(elem.element, reader.store).read(reader); + }); this._renderTextEffect = derived(this, _reader => { const tm = this._editor.model.get()!; const origLine = tm.getLineContent(this._viewData.edit.range.startLineNumber); @@ -106,7 +112,7 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin }); const modifiedLineHeight = this._editor.observeLineHeightForPosition(this._viewData.edit.range.getStartPosition()); const altCount = observableFromPromise(this._viewData.alternativeAction?.count ?? new Promise(resolve => resolve(undefined))).map(c => c.value); - const altModifierActive = observableFromEvent(this, ModifierKeyEmitter.getInstance().event, () => ModifierKeyEmitter.getInstance().keyStatus.shiftKey); + const altModifierActive = derived(this, reader => this._userInteractionService.readModifierKeyStatus(this._editor.editor.getDomNode()!, reader).shiftKey); this._layout = derived(this, reader => { this._renderTextEffect.read(reader); const widgetStart = this._start.read(reader); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts index 580d401fa7a32..aa935f23b647d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts @@ -131,11 +131,11 @@ export function getEditorValidOverlayRect(editor: ObservableCodeEditor): IObserv const width = derived({ name: 'editor.validOverlay.width' }, r => { const hasMinimapOnTheRight = editor.layoutInfoMinimap.read(r).minimapLeft !== 0; - const editorWidth = editor.layoutInfoWidth.read(r) - contentLeft.read(r); + const editorWidth = Math.max(0, editor.layoutInfoWidth.read(r) - contentLeft.read(r)); if (hasMinimapOnTheRight) { const minimapAndScrollbarWidth = editor.layoutInfoMinimap.read(r).minimapWidth + editor.layoutInfoVerticalScrollbarWidth.read(r); - return editorWidth - minimapAndScrollbarWidth; + return Math.max(0, editorWidth - minimapAndScrollbarWidth); } return editorWidth; diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 6041f65a055b6..b55bf1c003802 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -44,6 +44,7 @@ import { IHoverService, WorkbenchHoverDelegate } from '../../../platform/hover/b import { setBaseLayerHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegate2.js'; import { IMarkdownRendererService } from '../../../platform/markdown/browser/markdownRenderer.js'; import { EditorMarkdownCodeBlockRenderer } from '../../browser/widget/markdownRenderer/browser/editorMarkdownCodeBlockRenderer.js'; +import { IUserInteractionService } from '../../../platform/userInteraction/browser/userInteractionService.js'; /** * Description of an action contribution @@ -283,10 +284,11 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, + @IUserInteractionService userInteractionService: IUserInteractionService, ) { const options = { ..._options }; options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel; - super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); + super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService, userInteractionService); if (keybindingService instanceof StandaloneKeybindingService) { this._standaloneKeybindingService = keybindingService; @@ -433,6 +435,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, + @IUserInteractionService userInteractionService: IUserInteractionService, ) { const options = { ..._options }; updateConfigurationService(configurationService, options, false); @@ -445,7 +448,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon } const _model: ITextModel | null | undefined = options.model; delete options.model; - super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, hoverService, keybindingService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService, markdownRendererService); + super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, hoverService, keybindingService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService, markdownRendererService, userInteractionService); this._configurationService = configurationService; this._standaloneThemeService = themeService; diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 61aa37410e22e..d56f1c23beace 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -101,6 +101,8 @@ import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorker import { StandaloneWebWorkerService } from './services/standaloneWebWorkerService.js'; import { IDefaultAccountService } from '../../../platform/defaultAccount/common/defaultAccount.js'; import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../base/common/defaultAccount.js'; +import { IUserInteractionService } from '../../../platform/userInteraction/browser/userInteractionService.js'; +import { UserInteractionService } from '../../../platform/userInteraction/browser/userInteractionServiceImpl.js'; class SimpleModel implements IResolvedTextEditorModel { @@ -1183,6 +1185,7 @@ registerSingleton(ILoggerService, NullLoggerService, InstantiationType.Eager); registerSingleton(IDataChannelService, NullDataChannelService, InstantiationType.Eager); registerSingleton(IDefaultAccountService, StandaloneDefaultAccountService, InstantiationType.Eager); registerSingleton(IRenameSymbolTrackerService, NullRenameSymbolTrackerService, InstantiationType.Eager); +registerSingleton(IUserInteractionService, UserInteractionService, InstantiationType.Eager); /** * We don't want to eagerly instantiate services because embedders get a one time chance diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index db5de437a44a5..adede23418063 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -64,6 +64,7 @@ import { TestTreeSitterLibraryService } from '../common/services/testTreeSitterL import { IInlineCompletionsService, InlineCompletionsService } from '../../browser/services/inlineCompletionsService.js'; import { EditorCommand } from '../../browser/editorExtensions.js'; import { IDataChannelService, NullDataChannelService } from '../../../platform/dataChannel/common/dataChannel.js'; +import { IUserInteractionService, MockUserInteractionService } from '../../../platform/userInteraction/browser/userInteractionService.js'; export interface ITestCodeEditor extends IActiveCodeEditor { getViewModel(): ViewModel | undefined; @@ -247,6 +248,7 @@ export function createCodeEditorServices(disposables: Pick { diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index d46df43686233..1d85c5ab430ae 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -197,18 +197,20 @@ export class StorageMainService extends Disposable implements IStorageMainServic profileStorage = this._register(this.createProfileStorage(profile)); this.mapProfileToStorage.set(profile.id, profileStorage); - const listener = this._register(profileStorage.onDidChangeStorage(e => this._onDidChangeProfileStorage.fire({ + // Don't use this._register() for listeners that are disposed early + // as it causes entries to accumulate in _store when storage is closed/reopened + const listener = profileStorage.onDidChangeStorage(e => this._onDidChangeProfileStorage.fire({ ...e, storage: profileStorage!, profile - }))); + })); - this._register(Event.once(profileStorage.onDidCloseStorage)(() => { + Event.once(profileStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed profile storage (${profile.name})`); this.mapProfileToStorage.delete(profile.id); listener.dispose(); - })); + }); } return profileStorage; @@ -242,11 +244,12 @@ export class StorageMainService extends Disposable implements IStorageMainServic workspaceStorage = this._register(this.createWorkspaceStorage(workspace)); this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage); - this._register(Event.once(workspaceStorage.onDidCloseStorage)(() => { + // Don't use this._register() for Event.once as it auto-disposes + Event.once(workspaceStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed workspace storage (${workspace.id})`); this.mapWorkspaceToStorage.delete(workspace.id); - })); + }); } return workspaceStorage; diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index ff72087bd53cc..b1bb35b06024d 100644 --- a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -188,6 +188,7 @@ suite('StorageMainService', function () { const workspaceStorage2 = storageMainService.workspaceStorage(workspace); notStrictEqual(workspaceStorage, workspaceStorage2); + await profileStorage2.close(); await workspaceStorage2.close(); }); diff --git a/src/vs/platform/userInteraction/browser/userInteractionService.ts b/src/vs/platform/userInteraction/browser/userInteractionService.ts new file mode 100644 index 0000000000000..bb505bedd7485 --- /dev/null +++ b/src/vs/platform/userInteraction/browser/userInteractionService.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { constObservable, IObservable, IReader } from '../../../base/common/observable.js'; +import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; +import { Emitter } from '../../../base/common/event.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { IFocusTracker } from '../../../base/browser/dom.js'; + +export const IUserInteractionService = createDecorator('userInteractionService'); + +export interface IModifierKeyStatus { + readonly ctrlKey: boolean; + readonly shiftKey: boolean; + readonly altKey: boolean; + readonly metaKey: boolean; +} + +/** + * Used to track user UI interactions such as focus and hover states. + * This allows mocking these interactions in tests and simulating specific states. + */ +export interface IUserInteractionService { + readonly _serviceBrand: undefined; + + /** + * Reads the current modifier key status for the window containing the given element. + * Pass an element to determine the correct window context (for multi-window support). + */ + readModifierKeyStatus(element: HTMLElement | Window, reader: IReader | undefined): IModifierKeyStatus; + + /** + * Creates an observable that tracks whether the given element (or a descendant) has focus. + * The observable is disposed when the disposable store is disposed. + */ + createFocusTracker(element: HTMLElement | Window, store: DisposableStore): IObservable; + + /** + * Creates an observable that tracks whether the given element is hovered. + * The observable is disposed when the disposable store is disposed. + */ + createHoverTracker(element: Element, store: DisposableStore): IObservable; + + createDomFocusTracker(element: HTMLElement): IFocusTracker; +} + +/** + * Mock implementation of IUserInteractionService that can be used for testing + * or simulating specific interaction states. + */ +export class MockUserInteractionService implements IUserInteractionService { + readonly _serviceBrand: undefined; + + constructor( + private readonly _simulateFocus: boolean = true, + private readonly _simulateHover: boolean = false, + private readonly _modifiers: IModifierKeyStatus = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false } + ) { } + + readModifierKeyStatus(_element: HTMLElement | Window, _reader: IReader | undefined): IModifierKeyStatus { + return this._modifiers; + } + + createFocusTracker(_element: HTMLElement | Window, _store: DisposableStore): IObservable { + return constObservable(this._simulateFocus); + } + + createHoverTracker(_element: Element, _store: DisposableStore): IObservable { + return constObservable(this._simulateHover); + } + + createDomFocusTracker(_element: HTMLElement): IFocusTracker { + const tracker = new class extends Disposable implements IFocusTracker { + private readonly _onDidFocus = this._register(new Emitter()); + readonly onDidFocus = this._onDidFocus.event; + private readonly _onDidBlur = this._register(new Emitter()); + readonly onDidBlur = this._onDidBlur.event; + refreshState(): void { } + fireFocus(): void { this._onDidFocus.fire(); } + }; + if (this._simulateFocus) { + queueMicrotask(() => tracker.fireFocus()); + } + return tracker; + } +} diff --git a/src/vs/platform/userInteraction/browser/userInteractionServiceImpl.ts b/src/vs/platform/userInteraction/browser/userInteractionServiceImpl.ts new file mode 100644 index 0000000000000..e0431c43297d6 --- /dev/null +++ b/src/vs/platform/userInteraction/browser/userInteractionServiceImpl.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getWindow, IFocusTracker, ModifierKeyEmitter, trackFocus } from '../../../base/browser/dom.js'; +import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { IObservable, IReader, observableFromEvent, observableValue } from '../../../base/common/observable.js'; +import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; +import { IModifierKeyStatus, IUserInteractionService } from './userInteractionService.js'; + +export class UserInteractionService implements IUserInteractionService { + readonly _serviceBrand: undefined; + + private readonly _modifierObservables = new WeakMap>(); + + readModifierKeyStatus(element: HTMLElement | Window, reader: IReader | undefined): IModifierKeyStatus { + const win = element instanceof Window ? element : getWindow(element); + let obs = this._modifierObservables.get(win); + if (!obs) { + const emitter = ModifierKeyEmitter.getInstance(); + obs = observableFromEvent( + this, + emitter.event, + () => ({ + ctrlKey: emitter.keyStatus.ctrlKey, + shiftKey: emitter.keyStatus.shiftKey, + altKey: emitter.keyStatus.altKey, + metaKey: emitter.keyStatus.metaKey, + }) + ); + this._modifierObservables.set(win, obs); + } + return obs.read(reader); + } + + createFocusTracker(element: HTMLElement | Window, store: DisposableStore): IObservable { + const tracker = store.add(trackFocus(element)); + const hasFocusWithin = (el: HTMLElement | Window): boolean => { + if (el instanceof Window) { + return el.document.hasFocus(); + } + const shadowRoot = el.getRootNode() instanceof ShadowRoot ? el.getRootNode() as ShadowRoot : null; + const activeElement = shadowRoot ? shadowRoot.activeElement : el.ownerDocument.activeElement; + return el.contains(activeElement); + }; + + const value = observableValue('isFocused', hasFocusWithin(element)); + store.add(tracker.onDidFocus(() => value.set(true, undefined))); + store.add(tracker.onDidBlur(() => value.set(false, undefined))); + return value; + } + + createHoverTracker(element: Element, store: DisposableStore): IObservable { + const value = observableValue('isHovered', false); + const onEnter = () => value.set(true, undefined); + const onLeave = () => value.set(false, undefined); + element.addEventListener('mouseenter', onEnter); + element.addEventListener('mouseleave', onLeave); + store.add({ + dispose: () => { + element.removeEventListener('mouseenter', onEnter); + element.removeEventListener('mouseleave', onLeave); + } + }); + return value; + } + + createDomFocusTracker(element: HTMLElement): IFocusTracker { + return trackFocus(element); + } +} + +registerSingleton(IUserInteractionService, UserInteractionService, InstantiationType.Delayed); diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 61dfad33c802e..f641d14eac276 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -14,7 +14,9 @@ import { Registry } from '../../../platform/registry/common/platform.js'; import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; -import { createStringDataTransferItem, VSDataTransfer } from '../../../base/common/dataTransfer.js'; +import { createStringDataTransferItem, UriList, VSDataTransfer } from '../../../base/common/dataTransfer.js'; +import { Mimes } from '../../../base/common/mime.js'; +import { URI } from '../../../base/common/uri.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { DataTransferFileCache } from '../common/shared/dataTransferCache.js'; import * as typeConvert from '../common/extHostTypeConverters.js'; @@ -264,7 +266,11 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController { const additionalDataTransfer = new VSDataTransfer(); additionalDataTransferDTO.items.forEach(([type, item]) => { - additionalDataTransfer.replace(type, createStringDataTransferItem(item.asString)); + // For text/uri-list, reconstruct from uriListData which has been transformed by the URI transformer + const value = type === Mimes.uriList && item.uriListData + ? UriList.create(item.uriListData.map(part => typeof part === 'string' ? part : URI.revive(part))) + : item.asString; + additionalDataTransfer.replace(type, createStringDataTransferItem(value)); }); return additionalDataTransfer; } diff --git a/src/vs/workbench/api/common/shared/tasks.ts b/src/vs/workbench/api/common/shared/tasks.ts index 0d8faae5d0dc7..a7e46e70ff8ad 100644 --- a/src/vs/workbench/api/common/shared/tasks.ts +++ b/src/vs/workbench/api/common/shared/tasks.ts @@ -10,7 +10,7 @@ import { ITaskExecution } from '../../../contrib/tasks/common/tasks.js'; export interface ITaskDefinitionDTO { type: string; - [name: string]: any; + [name: string]: unknown; } export interface ITaskPresentationOptionsDTO { diff --git a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts index dc20abb8c5e8a..368654f06943f 100644 --- a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts @@ -14,7 +14,7 @@ import { TestNotificationService } from '../../../../platform/notification/test/ import { Registry } from '../../../../platform/registry/common/platform.js'; import { NullTelemetryService } from '../../../../platform/telemetry/common/telemetryUtils.js'; import { MainThreadTreeViews } from '../../browser/mainThreadTreeViews.js'; -import { ExtHostTreeViewsShape } from '../../common/extHost.protocol.js'; +import { DataTransferDTO, ExtHostTreeViewsShape } from '../../common/extHost.protocol.js'; import { CustomTreeView } from '../../../browser/parts/views/treeView.js'; import { Extensions, ITreeItem, ITreeView, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptorService, IViewsRegistry, TreeItemCollapsibleState, ViewContainer, ViewContainerLocation } from '../../../common/views.js'; import { IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js'; @@ -22,6 +22,9 @@ import { ExtensionHostKind } from '../../../services/extensions/common/extension import { ViewDescriptorService } from '../../../services/views/browser/viewDescriptorService.js'; import { TestViewsService, workbenchInstantiationService } from '../../../test/browser/workbenchTestServices.js'; import { TestExtensionService } from '../../../test/common/workbenchTestServices.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Mimes } from '../../../../base/common/mime.js'; +import { URI } from '../../../../base/common/uri.js'; suite('MainThreadHostTreeView', function () { const testTreeViewId = 'testTreeView'; @@ -47,6 +50,7 @@ suite('MainThreadHostTreeView', function () { let container: ViewContainer; let mainThreadTreeViews: MainThreadTreeViews; let extHostTreeViewsShape: MockExtHostTreeViewsShape; + let instantiationService: TestInstantiationService; teardown(() => { ViewsRegistry.deregisterViews(ViewsRegistry.getViews(container), container); @@ -55,7 +59,7 @@ suite('MainThreadHostTreeView', function () { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(async () => { - const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); + instantiationService = workbenchInstantiationService(undefined, disposables); const viewDescriptorService = disposables.add(instantiationService.createInstance(ViewDescriptorService)); instantiationService.stub(IViewDescriptorService, viewDescriptorService); // eslint-disable-next-line local/code-no-any-casts @@ -93,5 +97,91 @@ suite('MainThreadHostTreeView', function () { assert((children![0]).customProp === customValue, 'Tree Items should keep custom properties'); }); + test('handleDrag reconstructs URI list from uriListData', async () => { + const testTreeViewIdWithDrag = 'testTreeViewWithDrag'; + + // Create a mock that returns URI list data + const mockExtHostWithDrag = new class extends mock() { + override async $getChildren(treeViewId: string, treeItemHandle?: string[]): Promise<(number | ITreeItem)[][]> { + return [[0, { handle: 'item1', collapsibleState: TreeItemCollapsibleState.None }]]; + } + + override async $hasResolve(): Promise { + return false; + } + + override $setVisible(): void { } + + override async $handleDrag(_sourceViewId: string, _sourceTreeItemHandles: string[], _operationUuid: string, _token: CancellationToken): Promise { + // Return a DataTransferDTO with text/uri-list containing uriListData + // This simulates what the extension host sends after URI transformation + return { + items: [ + [Mimes.uriList, { + id: 'test-id', + // This is the original (untransformed) string - should NOT be used + asString: 'file:///original/untransformed/path.txt', + fileData: undefined, + // This is the transformed URI data - should be used + uriListData: [ + { scheme: 'file', authority: '', path: '/transformed/correct/path.txt', query: '', fragment: '' } + ] + }] + ] + }; + } + }(); + + // Register a view with drag support + const viewDescriptorWithDrag: ITreeViewDescriptor = { + id: testTreeViewIdWithDrag, + ctorDescriptor: null!, + name: nls.localize2('Test View 2', 'Test View 2'), + treeView: disposables.add(instantiationService.createInstance(CustomTreeView, 'testTree2', 'Test Title 2', 'extension.id')), + }; + ViewsRegistry.registerViews([viewDescriptorWithDrag], container); + + const dragTestExtensionService = new TestExtensionService(); + const dragTestMainThreadTreeViews = disposables.add(new MainThreadTreeViews( + new class implements IExtHostContext { + remoteAuthority = ''; + extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } + assertRegistered() { } + set(v: any): any { return null; } + getProxy(): any { + return mockExtHostWithDrag; + } + drain(): any { return null; } + }, new TestViewsService(), new TestNotificationService(), dragTestExtensionService, new NullLogService(), NullTelemetryService)); + dragTestMainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewIdWithDrag, { + showCollapseAll: false, + canSelectMany: false, + dropMimeTypes: [], + dragMimeTypes: [Mimes.uriList], + hasHandleDrag: true, + hasHandleDrop: false, + manuallyManageCheckboxes: false + }); + await dragTestExtensionService.whenInstalledExtensionsRegistered(); + + // Get the tree view and its drag controller + const dragTestTreeView: ITreeView = (ViewsRegistry.getView(testTreeViewIdWithDrag)).treeView; + const dragController = dragTestTreeView.dragAndDropController; + assert(dragController, 'Drag controller should exist'); + + // Call handleDrag + const result = await dragController.handleDrag(['item1'], 'test-operation-uuid', CancellationToken.None); + assert(result, 'Result should not be undefined'); + + // Verify that the URI list was reconstructed from uriListData, not asString + const uriListItem = result.get(Mimes.uriList); + assert(uriListItem, 'URI list item should exist'); + + const uriListValue = await uriListItem.asString(); + // The value should be the transformed URI, not the original untransformed one + assert.strictEqual(uriListValue, URI.from({ scheme: 'file', authority: '', path: '/transformed/correct/path.txt', query: '', fragment: '' }).toString()); + }); + }); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 3a2faf87d04e2..fd15522971e01 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -1411,13 +1411,15 @@ function registerModalEditorCommands(): void { constructor() { super({ id: MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, - title: localize2('moveToMainWindow', 'Open as Editor'), + title: localize2('moveToMainWindow', 'Open Modal Editor in Main Window'), + category: Categories.View, + f1: true, icon: Codicon.openInProduct, precondition: EditorPartModalContext, menu: { id: MenuId.ModalEditorTitle, group: 'navigation', - order: 1 + order: 0 } }); } @@ -1438,6 +1440,8 @@ function registerModalEditorCommands(): void { super({ id: CLOSE_MODAL_EDITOR_COMMAND_ID, title: localize2('closeModalEditor', 'Close Modal Editor'), + category: Categories.View, + f1: true, icon: Codicon.close, precondition: EditorPartModalContext, keybinding: { diff --git a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css index f7acd3ff3a4cc..de3bc1e1f33ec 100644 --- a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css +++ b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css @@ -10,8 +10,8 @@ width: 100%; left: 0; top: 0; - /* z-index cannot be above iframes (50) to support showing them */ - z-index: 40; + /* z-index for modal editors: below dialogs, quick input, context views, hovers but above other things */ + z-index: 2000; display: flex; justify-content: center; align-items: center; @@ -52,6 +52,7 @@ height: 32px; min-height: 32px; padding: 0 8px 0 16px; + color: var(--vscode-titleBar-activeForeground); background-color: var(--vscode-titleBar-activeBackground); border-bottom: 1px solid var(--vscode-titleBar-border, transparent); } @@ -77,6 +78,10 @@ gap: 4px; } +.monaco-modal-editor-block .modal-editor-action-container .actions-container .codicon { + color: inherit; +} + /** Modal Editor Part: Ensure proper sizing */ .monaco-modal-editor-block .modal-editor-part .content { flex: 1; diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 023a7116c1e87..ab5be6496b2de 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -95,10 +95,14 @@ export class ModalEditorPart { menuOptions: { shouldForwardArgs: true } })); - // Update title when active editor changes disposables.add(Event.runAndSubscribe(modalEditorService.onDidActiveEditorChange, (() => { + + // Update title when active editor changes const activeEditor = editorPart.activeGroup.activeEditor; titleElement.textContent = activeEditor?.getTitle(Verbosity.MEDIUM) ?? ''; + + // Notify editor part that active editor changed + editorPart.notifyActiveEditorChanged(); }))); // Handle close on click outside (on the dimmed background) @@ -157,22 +161,30 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { @IStorageService storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IHostService hostService: IHostService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, ) { const id = ModalEditorPartImpl.COUNTER++; super(editorPartsView, `workbench.parts.modalEditor.${id}`, groupsLabel, windowId, instantiationService, themeService, configurationService, storageService, layoutService, hostService, contextKeyService); - // Enforce some editor part options for modal editors + this.enforceModalPartOptions(); + } + + private enforceModalPartOptions(): void { + const editorCount = this.groups.reduce((count, group) => count + group.count, 0); this.optionsDisposable.value = this.enforcePartOptions({ - showTabs: 'none', + showTabs: editorCount > 1 ? 'multiple' : 'none', closeEmptyGroups: true, - tabActionCloseVisibility: false, + tabActionCloseVisibility: editorCount > 1, editorActionsLocation: 'default', tabHeight: 'default', wrapTabs: false }); } + notifyActiveEditorChanged(): void { + this.enforceModalPartOptions(); + } + protected override handleContextKeys(): void { const isModalEditorPartContext = EditorPartModalContext.bindTo(this.scopedContextKeyService); isModalEditorPartContext.set(true); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 428c63041da5f..bebcf70f3b357 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -93,6 +93,7 @@ export class AccessibleView extends Disposable { private _currentContent: string | undefined; private _lastProvider: AccesibleViewContentProvider | undefined; + private _lastProviderPosition: Map = new Map(); private _viewContainer: HTMLElement | undefined; @@ -299,6 +300,13 @@ export class AccessibleView extends Disposable { onHide: () => { if (!showAccessibleViewHelp) { this._updateLastProvider(); + // Save cursor position before disposing so it can be restored on reopen + if (this._currentProvider) { + const currentPosition = this._editorWidget.getPosition(); + if (currentPosition) { + this._lastProviderPosition.set(this._currentProvider.id, currentPosition); + } + } this._currentProvider?.dispose(); this._currentProvider = undefined; this._resetContextKeys(); @@ -323,6 +331,7 @@ export class AccessibleView extends Disposable { if (this._lastProvider?.options.id === id) { this._lastProvider = undefined; } + this._lastProviderPosition.delete(id); })); } if (provider.options.id) { @@ -640,6 +649,17 @@ export class AccessibleView extends Disposable { } } else if (previousPosition) { this._editorWidget.setPosition(previousPosition); + } else { + // Restore the saved position for this provider if available (e.g., after close and reopen) + const savedPosition = this._lastProviderPosition.get(provider.id); + if (savedPosition) { + const lineCount = this._editorWidget.getModel()?.getLineCount() ?? 0; + // Only restore if the saved position is still valid within the current content + if (savedPosition.lineNumber <= lineCount) { + this._editorWidget.setPosition(savedPosition); + this._editorWidget.revealPosition(savedPosition); + } + } } }); this._updateToolbar(this._currentProvider.actions, provider.options.type); @@ -661,6 +681,11 @@ export class AccessibleView extends Disposable { return; } this._updateContextKeys(provider, false); + // Save the cursor position for this provider so it can be restored on reopen + const currentPosition = this._editorWidget.getPosition(); + if (currentPosition) { + this._lastProviderPosition.set(provider.id, currentPosition); + } this._lastProvider = undefined; this._currentContent = undefined; this._currentProvider?.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts index a47f6a6e842dc..f23dcdad5ae67 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts @@ -12,7 +12,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IEditorGroupsService, IEditorWorkingSet } from '../../../../../services/editor/common/editorGroupsService.js'; -import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { IEditorService, MODAL_GROUP } from '../../../../../services/editor/common/editorService.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IAgentSession, isSessionInProgressStatus } from '../agentSessionsModel.js'; import { IChatWidgetService } from '../../chat.js'; @@ -213,17 +213,17 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this.logService.trace(`[AgentSessionProjection] Found ${diffResources.length} files with diffs to display`); if (diffResources.length > 0) { - // Clear editors only when we know we have content to display - await this.editorGroupsService.applyWorkingSet('empty', { preserveFocus: true }); - - // Open multi-diff editor showing all changes - await this.commandService.executeCommand('_workbench.openMultiDiffEditor', { - multiDiffSourceUri: session.resource.with({ scheme: session.resource.scheme + '-agent-session-projection' }), - title: localize('agentSessionProjection.changes.title', '{0} - All Changes', session.label), - resources: diffResources, - }); - - this.logService.trace(`[AgentSessionProjection] Multi-diff editor opened successfully`); + // Open multi-diff editor showing all changes in a modal editor + await this.editorService.openEditor({ + multiDiffSource: session.resource.with({ scheme: session.resource.scheme + '-agent-session-projection' }), + resources: diffResources.map(dr => ({ + original: { resource: dr.originalUri }, + modified: { resource: dr.modifiedUri } + })), + label: localize('agentSessionProjection.changes.title', '{0} - All Changes', session.label), + }, MODAL_GROUP); + + this.logService.trace(`[AgentSessionProjection] Multi-diff editor opened successfully in modal view`); // Save this as the session's working set const sessionKey = session.resource.toString(); @@ -336,8 +336,6 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS let filesOpened = false; if (session.providerType === AgentSessionProviders.Local) { // Local sessions use editing session for changes - we already verified hasUndecidedChanges above - // Clear editors to prepare for the changes view - await this.editorGroupsService.applyWorkingSet('empty', { preserveFocus: true }); filesOpened = true; } else { // Try to open session files - only continue with projection if files were displayed diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index f64478a7270fc..e0df9ffe04f2d 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -61,6 +61,7 @@ import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { IChatContentReference } from '../../common/chatService/chatService.js'; import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry, isStringVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../common/languageModels.js'; +import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js'; import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js'; import { IChatContextService } from '../contextContrib/chatContextService.js'; @@ -196,6 +197,7 @@ function modelSupportsVision(currentLanguageModel: ILanguageModelChatMetadataAnd return currentLanguageModel?.metadata.capabilities?.vision ?? false; } + export class FileAttachmentWidget extends AbstractChatAttachmentWidget { constructor( @@ -389,6 +391,7 @@ export class ImageAttachmentWidget extends AbstractChatAttachmentWidget { @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @IInstantiationService instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, ) { super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService); @@ -412,7 +415,7 @@ export class ImageAttachmentWidget extends AbstractChatAttachmentWidget { const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : 'Current model'; const fullName = resource ? this.labelService.getUriLabel(resource) : (attachment.fullName || attachment.name); - this._register(createImageElements(resource, attachment.name, fullName, this.element, attachment.value as Uint8Array, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState)); + this._register(createImageElements(resource, attachment.name, fullName, this.element, attachment.value as Uint8Array, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState, this.chatEntitlementService.previewFeaturesDisabled)); if (resource) { this.addResourceOpenHandlers(resource, undefined); @@ -430,7 +433,8 @@ function createImageElements(resource: URI | undefined, name: string, fullName: currentLanguageModelName: string | undefined, clickHandler: () => void, currentLanguageModel?: ILanguageModelChatMetadataAndIdentifier, - omittedState?: OmittedState): IDisposable { + omittedState?: OmittedState, + previewFeaturesDisabled?: boolean): IDisposable { const disposable = new DisposableStore(); if (omittedState === OmittedState.Partial) { @@ -445,7 +449,7 @@ function createImageElements(resource: URI | undefined, name: string, fullName: disposable.add(dom.addDisposableListener(element, 'click', clickHandler)); } const supportsVision = modelSupportsVision(currentLanguageModel); - const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(supportsVision ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning')); + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$((supportsVision && !previewFeaturesDisabled) ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning')); const textLabel = dom.$('span.chat-attached-context-custom-text', {}, name); element.appendChild(pillIcon); element.appendChild(textLabel); @@ -453,7 +457,14 @@ function createImageElements(resource: URI | undefined, name: string, fullName: const hoverElement = dom.$('div.chat-attached-context-hover'); hoverElement.setAttribute('aria-label', ariaLabel); - if ((!supportsVision && currentLanguageModel) || omittedState === OmittedState.Full) { + if (previewFeaturesDisabled) { + element.classList.add('warning'); + hoverElement.textContent = localize('chat.imageAttachmentPreviewFeaturesDisabled', "Vision is disabled by your organization."); + disposable.add(hoverService.setupDelayedHover(element, { + content: hoverElement, + style: HoverStyle.Pointer, + })); + } else if ((!supportsVision && currentLanguageModel) || omittedState === OmittedState.Full) { element.classList.add('warning'); hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", currentLanguageModelName ?? 'This model'); disposable.add(hoverService.setupDelayedHover(element, { @@ -824,6 +835,7 @@ export class NotebookCellOutputChatAttachmentWidget extends AbstractChatAttachme @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @INotebookService private readonly notebookService: INotebookService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, ) { super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService); @@ -884,7 +896,7 @@ export class NotebookCellOutputChatAttachmentWidget extends AbstractChatAttachme const clickHandler = async () => await this.openResource(resource, { editorOptions: { preserveFocus: true } }, false, undefined); const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : undefined; const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array(); - this._register(createImageElements(resource, attachment.name, attachment.name, this.element, buffer, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState)); + this._register(createImageElements(resource, attachment.name, attachment.name, this.element, buffer, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState, this.chatEntitlementService.previewFeaturesDisabled)); } private getOutputItem(resource: URI, attachment: INotebookOutputVariableEntry) { diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts index ac9da62529bc8..fa7c12678e68b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts @@ -11,10 +11,10 @@ import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Lazy } from '../../../../../base/common/lazy.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../../nls.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import product from '../../../../../platform/product/common/product.js'; @@ -49,6 +49,7 @@ import { IMarker, IMarkerService, MarkerSeverity } from '../../../../../platform import { ChatSetupController } from './chatSetupController.js'; import { ChatSetupAnonymous, ChatSetupStep, IChatSetupResult } from './chatSetup.js'; import { ChatSetup } from './chatSetupRunner.js'; +import { chatViewsWelcomeRegistry } from '../viewsWelcome/chatViewsWelcome.js'; import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; import { IHostService } from '../../../../services/host/browser/host.js'; @@ -191,6 +192,7 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @IViewsService private readonly viewsService: IViewsService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); @@ -321,9 +323,12 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { }); }, 10000); + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => clearTimeout(timeoutHandle))); try { const ready = await Promise.race([ timeout(this.environmentService.remoteAuthority ? 60000 /* increase for remote scenarios */ : 20000).then(() => 'timedout'), + this.whenPanelAgentHasGuidance(disposables).then(() => 'panelGuidance'), Promise.allSettled([ whenAgentActivated, whenAgentReady, @@ -332,6 +337,20 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { ]) ]); + if (ready === 'panelGuidance') { + const warningMessage = localize('chatTookLongWarningExtension', "Please try again."); + + progress({ + kind: 'markdownContent', + content: new MarkdownString(warningMessage) + }); + + // This means Chat is unhealthy and we cannot retry the + // request. Signal this to the outside via an event. + this._onUnresolvableError.fire(); + return; + } + if (ready === 'timedout') { let warningMessage: string; if (this.chatEntitlementService.anonymous) { @@ -459,7 +478,7 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { return; } } finally { - clearTimeout(timeoutHandle); + disposables.dispose(); } } @@ -470,6 +489,25 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { }); } + private async whenPanelAgentHasGuidance(disposables: DisposableStore): Promise { + const panelAgentHasGuidance = () => chatViewsWelcomeRegistry.get().some(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when)); + + if (panelAgentHasGuidance()) { + return; + } + + return new Promise(resolve => { + disposables.add(Event.any( + chatViewsWelcomeRegistry.onDidChange, + Event.map(this.contextKeyService.onDidChangeContext, () => { }) + )(() => { + if (panelAgentHasGuidance()) { + resolve(); + } + })); + }); + } + private whenLanguageModelReady(languageModelsService: ILanguageModelsService, modelId: string | undefined): Promise | void { const hasModelForRequest = () => { if (modelId) { diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 5536b0defbef9..0be7666e97804 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -34,7 +34,7 @@ import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; -import { IPreToolUseCallerInput, IPreToolUseHookResult } from '../../common/hooks/hooksTypes.js'; +import { IPostToolUseCallerInput, IPreToolUseCallerInput, IPreToolUseHookResult } from '../../common/hooks/hooksTypes.js'; import { IHooksExecutionService } from '../../common/hooks/hooksExecutionService.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js'; @@ -45,7 +45,7 @@ import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IChatModel, IChatRequestModel } from '../../common/model/chatModel.js'; import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; -import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, isToolSet, ToolDataSource, toolMatchesModel, ToolSet, VSCodeToolReference, IToolSet, ToolSetForModel, IToolInvokedEvent } from '../../common/tools/languageModelToolsService.js'; +import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, isToolSet, ToolDataSource, toolContentToA11yString, toolMatchesModel, ToolSet, VSCodeToolReference, IToolSet, ToolSetForModel, IToolInvokedEvent } from '../../common/tools/languageModelToolsService.js'; import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js'; import { URI } from '../../../../../base/common/uri.js'; import { chatSessionResourceToId } from '../../common/model/chatUri.js'; @@ -422,6 +422,44 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return { hookResult }; } + /** + * Execute the postToolUse hook after tool completion. + * If the hook returns a "block" decision, additional context is appended to the tool result + * as feedback for the agent indicating the block and reason. The tool has already run, + * so blocking only provides feedback. + */ + private async _executePostToolUseHook( + dto: IToolInvocation, + toolResult: IToolResult, + token: CancellationToken + ): Promise { + if (!dto.context?.sessionResource) { + return; + } + + const hookInput: IPostToolUseCallerInput = { + toolName: dto.toolId, + toolInput: dto.parameters, + getToolResponseText: () => toolContentToA11yString(toolResult.content), + toolCallId: dto.callId, + }; + const hookResult = await this._hooksExecutionService.executePostToolUseHook(dto.context.sessionResource, hookInput, token); + + if (hookResult?.decision === 'block') { + const hookReason = hookResult.reason ?? localize('postToolUseHookBlockedNoReason', "Hook blocked tool result"); + this._logService.debug(`[LanguageModelToolsService#invokeTool] PostToolUse hook blocked for tool ${dto.toolId}: ${hookReason}`); + const blockMessage = localize('postToolUseHookBlockedContext', "The PostToolUse hook blocked this tool result. Reason: {0}", hookReason); + toolResult.content.push({ kind: 'text', value: '\n\n' + blockMessage + '\n' }); + } + + if (hookResult?.additionalContext) { + // Append additional context from all hooks to the tool result content + for (const context of hookResult.additionalContext) { + toolResult.content.push({ kind: 'text', value: '\n\n' + context + '\n' }); + } + } + } + async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`); @@ -569,6 +607,10 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return toolResult; } + if (userConfirmed.type === ToolConfirmKind.UserAction && userConfirmed.selectedButton) { + dto.selectedCustomButton = userConfirmed.selectedButton; + } + if (dto.toolSpecificData?.kind === 'input') { dto.parameters = dto.toolSpecificData.rawInput; dto.toolSpecificData = undefined; @@ -621,6 +663,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } } + // Execute postToolUse hook after successful tool execution + await this._executePostToolUseHook(dto, toolResult, token); + this._telemetryService.publicLog2( 'languageModelToolInvoked', { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts index 19312386303ef..b54e72a3215fa 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts @@ -54,29 +54,46 @@ export abstract class AbstractToolConfirmationSubPart extends BaseChatToolInvoca } protected render(config: IToolConfirmationConfig) { const { keybindingService, languageModelToolsService, toolInvocation } = this; - const allowTooltip = keybindingService.appendKeybinding(config.allowLabel, config.allowActionId); - const skipTooltip = keybindingService.appendKeybinding(config.skipLabel, config.skipActionId); + const state = toolInvocation.state.get(); + const customButtons = state.type === IChatToolInvocation.StateKind.WaitingForConfirmation + ? state.confirmationMessages?.customButtons + : undefined; - const additionalActions = this.additionalPrimaryActions(); - const buttons: IChatConfirmationButton<(() => void)>[] = [ - { - label: config.allowLabel, - tooltip: allowTooltip, + let buttons: IChatConfirmationButton<(() => void)>[]; + + if (customButtons && customButtons.length > 0) { + buttons = customButtons.map((label, index) => ({ + label, data: () => { - this.confirmWith(toolInvocation, { type: ToolConfirmKind.UserAction }); + this.confirmWith(toolInvocation, { type: ToolConfirmKind.UserAction, selectedButton: label }); }, - moreActions: additionalActions.length > 0 ? additionalActions : undefined, - }, - { - label: localize('skip', "Skip"), - tooltip: skipTooltip, - data: () => { - this.confirmWith(toolInvocation, { type: ToolConfirmKind.Skipped }); + isSecondary: index > 0, + })); + } else { + const allowTooltip = keybindingService.appendKeybinding(config.allowLabel, config.allowActionId); + const skipTooltip = keybindingService.appendKeybinding(config.skipLabel, config.skipActionId); + + const additionalActions = this.additionalPrimaryActions(); + buttons = [ + { + label: config.allowLabel, + tooltip: allowTooltip, + data: () => { + this.confirmWith(toolInvocation, { type: ToolConfirmKind.UserAction }); + }, + moreActions: additionalActions.length > 0 ? additionalActions : undefined, }, - isSecondary: true, - } - ]; + { + label: localize('skip', "Skip"), + tooltip: skipTooltip, + data: () => { + this.confirmWith(toolInvocation, { type: ToolConfirmKind.Skipped }); + }, + isSecondary: true, + } + ]; + } const contentElement = this.createContentElement(); const tool = languageModelToolsService.getTool(toolInvocation.toolId); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 5792cfb7da8ab..00a7702cb3166 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -107,6 +107,7 @@ import { RunSubagentTool } from '../../common/tools/builtinTools/runSubagentTool import { isEqual } from '../../../../../base/common/resources.js'; import { IChatTipService } from '../chatTipService.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; +import { ChatPendingDragController } from './chatPendingDragAndDrop.js'; const $ = dom.$; @@ -124,6 +125,9 @@ export interface IChatListItemTemplate { */ renderedPartsMounted?: boolean; + /** Drag handle element for reordering pending requests, if currently rendered. */ + dragHandle?: HTMLElement; + readonly rowContainer: HTMLElement; readonly titleToolbar?: MenuWorkbenchToolBar; readonly header?: HTMLElement; @@ -278,6 +282,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer p.kind === element.pendingKind).length; + if (sameKindCount > 1) { + const handle = dom.$('.chat-pending-drag-handle' + ThemeIcon.asCSSSelector(Codicon.gripper)); + templateData.rowContainer.prepend(handle); + templateData.dragHandle = handle; + this._pendingDragController.attachDragHandle(element, handle, templateData.rowContainer, templateData.elementDisposables); + } + } + if (element.id === this.viewModel?.editing?.id) { this._onDidRerender.fire(templateData); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index 61a26fa93430f..4e33ef0c176e9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -34,6 +34,7 @@ import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileT import { CodeBlockPart } from './chatContentParts/codeBlockPart.js'; import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; +import { ChatPendingDragController } from './chatPendingDragAndDrop.js'; export interface IChatListWidgetStyles { listForeground?: string; @@ -350,6 +351,11 @@ export class ChatListWidget extends Disposable { } })); + // Create drag-and-drop controller for reordering pending requests + this._renderer.pendingDragController = this._register( + scopedInstantiationService.createInstance(ChatPendingDragController, this._container, () => this._viewModel) + ); + // Create tree const styles = options.styles ?? {}; this._tree = this._register(scopedInstantiationService.createInstance( diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatPendingDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/widget/chatPendingDragAndDrop.ts new file mode 100644 index 0000000000000..8d29c508e3e6d --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatPendingDragAndDrop.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../base/browser/dom.js'; +import { DragAndDropObserver } from '../../../../../base/browser/dom.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js'; +import { IChatPendingRequest } from '../../common/model/chatModel.js'; +import { IChatRequestViewModel, IChatViewModel } from '../../common/model/chatViewModel.js'; + +const PENDING_REQUEST_ID_ATTR = 'data-pending-request-id'; +const PENDING_KIND_ATTR = 'data-pending-kind'; +const DRAGGING_CLASS = 'chat-pending-dragging'; + +interface IDragState { + readonly element: IChatRequestViewModel; + readonly pendingKind: ChatRequestQueueKind; +} + +/** + * Manages drag-and-drop reordering for pending (steering/queued) chat messages. + * Attaches drag handles to pending request rows and uses event delegation on + * the list container to handle drop targets, keeping logic isolated from the + * renderer itself. + */ +export class ChatPendingDragController extends Disposable { + + private _dragState: IDragState | undefined; + private readonly _insertIndicator: HTMLElement; + + constructor( + listContainer: HTMLElement, + private readonly _getViewModel: () => IChatViewModel | undefined, + @IChatService private readonly _chatService: IChatService, + ) { + super(); + + this._insertIndicator = dom.$('.chat-pending-insert-indicator'); + listContainer.append(this._insertIndicator); + this._register(toDisposable(() => this._insertIndicator.remove())); + + this._register(new DragAndDropObserver(listContainer, { + onDragOver: (e) => this._onDragOver(e), + onDragLeave: () => this._hideIndicator(), + onDragEnd: () => this._onDragEnd(), + onDrop: (e) => this._onDrop(e), + })); + } + + /** + * Called by the renderer to wire up a drag handle for a pending request row. + */ + attachDragHandle( + element: IChatRequestViewModel, + handleEl: HTMLElement, + rowContainer: HTMLElement, + disposables: DisposableStore, + ): void { + handleEl.setAttribute('draggable', 'true'); + + disposables.add(dom.addDisposableListener(handleEl, dom.EventType.DRAG_START, (e: DragEvent) => { + if (!e.dataTransfer || !element.pendingKind) { + return; + } + + this._dragState = { element, pendingKind: element.pendingKind }; + rowContainer.classList.add(DRAGGING_CLASS); + + // Use the row as the drag image + e.dataTransfer.setDragImage(rowContainer, 0, 0); + e.dataTransfer.effectAllowed = 'move'; + })); + + disposables.add(dom.addDisposableListener(handleEl, dom.EventType.DRAG_END, () => { + rowContainer.classList.remove(DRAGGING_CLASS); + this._onDragEnd(); + })); + } + + // --- drag event handlers (delegated on the container) --- + + private _onDragOver(e: DragEvent): void { + if (!this._dragState) { + return; + } + + const target = this._findDropTarget(e); + if (!target) { + this._hideIndicator(); + return; + } + + e.preventDefault(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'move'; + } + + const rect = target.row.getBoundingClientRect(); + const midY = rect.top + rect.height / 2; + const before = e.clientY < midY; + + this._showIndicator(target.row, before); + } + + private _onDrop(e: DragEvent): void { + this._hideIndicator(); + if (!this._dragState) { + return; + } + + const target = this._findDropTarget(e); + if (!target) { + return; + } + + e.preventDefault(); + + const rect = target.row.getBoundingClientRect(); + const midY = rect.top + rect.height / 2; + const insertBefore = e.clientY < midY; + + this._reorder(this._dragState.element, target.requestId, insertBefore); + this._dragState = undefined; + } + + private _onDragEnd(): void { + this._hideIndicator(); + this._dragState = undefined; + } + + // --- indicator positioning --- + + private _showIndicator(targetRow: HTMLElement, before: boolean): void { + const rect = targetRow.getBoundingClientRect(); + const parentRect = this._insertIndicator.parentElement!.getBoundingClientRect(); + this._insertIndicator.style.display = 'block'; + this._insertIndicator.style.left = `${rect.left - parentRect.left}px`; + this._insertIndicator.style.width = `${rect.width}px`; + this._insertIndicator.style.top = before + ? `${rect.top - parentRect.top}px` + : `${rect.bottom - parentRect.top}px`; + } + + private _hideIndicator(): void { + this._insertIndicator.style.display = 'none'; + } + + // --- target resolution --- + + private _findDropTarget(e: DragEvent): { row: HTMLElement; requestId: string } | undefined { + if (!this._dragState) { + return undefined; + } + + const target = (e.target as HTMLElement)?.closest?.(`[${PENDING_REQUEST_ID_ATTR}]`); + if (!target) { + return undefined; + } + + const requestId = target.getAttribute(PENDING_REQUEST_ID_ATTR)!; + const kind = target.getAttribute(PENDING_KIND_ATTR); + + // Only allow reorder within the same group + if (kind !== this._dragState.pendingKind || requestId === this._dragState.element.id) { + return undefined; + } + + return { row: target, requestId }; + } + + // --- reorder logic --- + + private _reorder(draggedElement: IChatRequestViewModel, targetId: string, insertBefore: boolean): void { + const viewModel = this._getViewModel(); + if (!viewModel) { + return; + } + + const pendingRequests = viewModel.model.getPendingRequests(); + const draggedKind = draggedElement.pendingKind!; + + // Split into the dragged kind's group and the rest (preserving order) + const group: IChatPendingRequest[] = []; + const rest: IChatPendingRequest[] = []; + for (const p of pendingRequests) { + (p.kind === draggedKind ? group : rest).push(p); + } + + // Remove dragged from group + const draggedIdx = group.findIndex(p => p.request.id === draggedElement.id); + if (draggedIdx === -1) { + return; + } + const [dragged] = group.splice(draggedIdx, 1); + + // Find target position and insert + let targetIdx = group.findIndex(p => p.request.id === targetId); + if (targetIdx === -1) { + return; + } + if (!insertBefore) { + targetIdx++; + } + group.splice(targetIdx, 0, dragged); + + // Rebuild full list: steering first, then queued (matching addPendingRequest ordering) + const reordered = (draggedKind === ChatRequestQueueKind.Steering + ? [...group, ...rest] // group is steering, rest is queued + : [...rest, ...group] // rest is steering, group is queued + ).map(p => ({ requestId: p.request.id, kind: p.kind })); + + this._chatService.setPendingRequests(viewModel.sessionResource, reordered); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index b8f69ea3084f1..952b37142f117 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2352,8 +2352,9 @@ have to be updated for changes to the rules above, or to support more deeply nes outline: none; border: none; - .codicon.codicon-file-media { + .codicon.codicon-file-media, .codicon.codicon-warning { font-size: 12px; + margin-right: 2px; } } @@ -2877,6 +2878,42 @@ have to be updated for changes to the rules above, or to support more deeply nes } } +/* Drag handle for reordering pending messages */ +.interactive-item-container .chat-pending-drag-handle { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: grab; + opacity: 0; + transition: opacity 0.15s; + color: var(--vscode-descriptionForeground); + margin-top: -4px; /* visually center with overlaid top controls */ +} + +.interactive-item-container.pending-request:hover .chat-pending-drag-handle { + opacity: 1; +} + +/* Dragging state */ +.interactive-item-container.chat-pending-dragging { + opacity: 0.4; +} + +/* Insertion indicator */ +.chat-pending-insert-indicator { + position: absolute; + height: 2px; + background: var(--vscode-focusBorder); + pointer-events: none; + z-index: 100; + display: none; +} + .interactive-item-container .chat-request-status { color: var(--vscode-descriptionForeground); font-size: var(--vscode-chat-font-size-body-xs); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts index 8cd698568b89f..53769e1c43cf5 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts @@ -84,8 +84,10 @@ export class ChatViewTitleControl extends Disposable { private render(parent: HTMLElement): void { const elements = h('div.chat-view-title-container', [ - h('div.chat-view-title-navigation-toolbar@navigationToolbar'), - h('div.chat-view-title-actions-toolbar@actionsToolbar'), + h('div.chat-view-title-inner', [ + h('div.chat-view-title-navigation-toolbar@navigationToolbar'), + h('div.chat-view-title-actions-toolbar@actionsToolbar'), + ]), ]); // Toolbar on the left diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css index 057afaa9074c9..7884cf2c51270 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css @@ -67,6 +67,13 @@ /* Sessions control: stacked */ .chat-viewpane.has-sessions-control.sessions-control-orientation-stacked { + .agent-sessions-container { + /* center it above the chat which has this max-width restriction */ + max-width: 950px; + margin: 0 auto; + width: 100%; + } + .agent-sessions-new-button-container { /* hide new session button when stacked */ display: none; diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css index 9434f08cef31d..06dcdce2ce77c 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css @@ -7,9 +7,18 @@ .chat-view-title-container { display: none; - align-items: center; cursor: pointer; + .chat-view-title-inner { + display: flex; + align-items: center; + /* center it above the chat which has this max-width restriction */ + max-width: 950px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; + } + .chat-view-title-navigation-toolbar { overflow: hidden; @@ -44,7 +53,7 @@ } .chat-view-title-container.visible { - display: flex; + display: block; } } @@ -62,28 +71,28 @@ &.chat-view-location-sidebar, &.chat-view-location-panel, &.chat-view-location-auxiliarybar { - .chat-view-title-container { + .chat-view-title-inner { padding: 0 12px 0 16px; } } /* Auxiliarybar with non-default activity bar position */ &.activity-bar-location-other.chat-view-location-auxiliarybar { - .chat-view-title-container { + .chat-view-title-inner { padding: 0 8px 0 16px; } } /* Side-by-side sessions: left position (any activity bar) */ &.has-sessions-control.sessions-control-orientation-sidebyside.chat-view-position-left { - .chat-view-title-container { + .chat-view-title-inner { padding: 0 8px; } } /* Side-by-side sessions: right position (default activity bar only) */ &.activity-bar-location-default.has-sessions-control.sessions-control-orientation-sidebyside.chat-view-position-right { - .chat-view-title-container { + .chat-view-title-inner { padding: 0 8px 0 16px; } } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 25d226f89b115..4c4a4b6ec7918 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -530,7 +530,7 @@ export type ConfirmedReason = | { type: ToolConfirmKind.ConfirmationNotNeeded; reason?: string | IMarkdownString } | { type: ToolConfirmKind.Setting; id: string } | { type: ToolConfirmKind.LmServicePerTool; scope: 'session' | 'workspace' | 'profile' } - | { type: ToolConfirmKind.UserAction } + | { type: ToolConfirmKind.UserAction; selectedButton?: string } | { type: ToolConfirmKind.Skipped }; export interface IChatToolInvocation { diff --git a/src/vs/workbench/contrib/chat/common/hooks/hooksCommandTypes.ts b/src/vs/workbench/contrib/chat/common/hooks/hooksCommandTypes.ts index 0ecb36b7cc431..d13e9afec1187 100644 --- a/src/vs/workbench/contrib/chat/common/hooks/hooksCommandTypes.ts +++ b/src/vs/workbench/contrib/chat/common/hooks/hooksCommandTypes.ts @@ -90,3 +90,31 @@ export interface IPreToolUseCommandOutput extends IHookCommandOutput { } //#endregion + +//#region PostToolUse Hook Types + +/** + * Tool-specific command input fields for postToolUse hook. + * These are mixed with IHookCommandInput at runtime. + */ +export interface IPostToolUseCommandInput { + readonly tool_name: string; + readonly tool_input: unknown; + readonly tool_response: string; + readonly tool_use_id: string; +} + +/** + * External command output for postToolUse hook. + * Extends common output with decision control fields. + */ +export interface IPostToolUseCommandOutput extends IHookCommandOutput { + readonly decision?: 'block'; + readonly reason?: string; + readonly hookSpecificOutput?: { + readonly hookEventName?: string; + readonly additionalContext?: string; + }; +} + +//#endregion diff --git a/src/vs/workbench/contrib/chat/common/hooks/hooksExecutionService.ts b/src/vs/workbench/contrib/chat/common/hooks/hooksExecutionService.ts index 20ec217b8827c..5a7b0431a0c2a 100644 --- a/src/vs/workbench/contrib/chat/common/hooks/hooksExecutionService.ts +++ b/src/vs/workbench/contrib/chat/common/hooks/hooksExecutionService.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { StopWatch } from '../../../../../base/common/stopwatch.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -17,13 +18,18 @@ import { HookCommandResultKind, IHookCommandInput, IHookCommandResult, + IPostToolUseCommandInput, IPreToolUseCommandInput } from './hooksCommandTypes.js'; import { commonHookOutputValidator, IHookResult, + IPostToolUseCallerInput, + IPostToolUseHookResult, IPreToolUseCallerInput, IPreToolUseHookResult, + postToolUseOutputValidator, + PreToolUsePermissionDecision, preToolUseOutputValidator } from './hooksTypes.js'; @@ -35,6 +41,14 @@ export interface IHooksExecutionOptions { readonly token?: CancellationToken; } +export interface IHookExecutedEvent { + readonly hookType: HookTypeValue; + readonly sessionResource: URI; + readonly input: unknown; + readonly results: readonly IHookResult[]; + readonly durationMs: number; +} + /** * Callback interface for hook execution proxies. * MainThreadHooks implements this to forward calls to the extension host. @@ -48,6 +62,11 @@ export const IHooksExecutionService = createDecorator('h export interface IHooksExecutionService { _serviceBrand: undefined; + /** + * Fires when a hook has finished executing. + */ + readonly onDidExecuteHook: Event; + /** * Called by mainThreadHooks when extension host is ready */ @@ -74,6 +93,14 @@ export interface IHooksExecutionService { * Returns a combined result with common fields and permission decision. */ executePreToolUseHook(sessionResource: URI, input: IPreToolUseCallerInput, token?: CancellationToken): Promise; + + /** + * Execute postToolUse hooks with typed input and validated output. + * Called after a tool completes successfully. The execution service builds the full hook input + * from the caller input plus session context. + * Returns a combined result with decision and additional context. + */ + executePostToolUseHook(sessionResource: URI, input: IPostToolUseCallerInput, token?: CancellationToken): Promise; } /** @@ -81,9 +108,12 @@ export interface IHooksExecutionService { */ const redactedInputKeys = ['toolArgs']; -export class HooksExecutionService implements IHooksExecutionService { +export class HooksExecutionService extends Disposable implements IHooksExecutionService { declare readonly _serviceBrand: undefined; + private readonly _onDidExecuteHook = this._register(new Emitter()); + readonly onDidExecuteHook: Event = this._onDidExecuteHook.event; + private _proxy: IHooksExecutionProxy | undefined; private readonly _sessionHooks = new Map(); private _channelRegistered = false; @@ -92,7 +122,9 @@ export class HooksExecutionService implements IHooksExecutionService { constructor( @ILogService private readonly _logService: ILogService, @IOutputService private readonly _outputService: IOutputService, - ) { } + ) { + super(); + } setProxy(proxy: IHooksExecutionProxy): void { this._proxy = proxy; @@ -248,43 +280,54 @@ export class HooksExecutionService implements IHooksExecutionService { } async executeHook(hookType: HookTypeValue, sessionResource: URI, options?: IHooksExecutionOptions): Promise { - if (!this._proxy) { - return []; - } + const sw = StopWatch.create(); + const results: IHookResult[] = []; - const hooks = this.getHooksForSession(sessionResource); - if (!hooks) { - return []; - } + try { + if (!this._proxy) { + return results; + } - const hookCommands = hooks[hookType]; - if (!hookCommands || hookCommands.length === 0) { - return []; - } + const hooks = this.getHooksForSession(sessionResource); + if (!hooks) { + return results; + } - const requestId = this._requestCounter++; - const token = options?.token ?? CancellationToken.None; + const hookCommands = hooks[hookType]; + if (!hookCommands || hookCommands.length === 0) { + return results; + } - this._logService.debug(`[HooksExecutionService] Executing ${hookCommands.length} hook(s) for type '${hookType}'`); - this._log(requestId, hookType, `Executing ${hookCommands.length} hook(s)`); + const requestId = this._requestCounter++; + const token = options?.token ?? CancellationToken.None; - const results: IHookResult[] = []; - for (const hookCommand of hookCommands) { - const result = await this._runSingleHook(requestId, hookType, hookCommand, sessionResource, options?.input, token); - results.push(result); - - // If stopReason is set, stop processing remaining hooks - if (result.stopReason) { - this._log(requestId, hookType, `Stopping: ${result.stopReason}`); - break; + this._logService.debug(`[HooksExecutionService] Executing ${hookCommands.length} hook(s) for type '${hookType}'`); + this._log(requestId, hookType, `Executing ${hookCommands.length} hook(s)`); + + for (const hookCommand of hookCommands) { + const result = await this._runSingleHook(requestId, hookType, hookCommand, sessionResource, options?.input, token); + results.push(result); + + // If stopReason is set, stop processing remaining hooks + if (result.stopReason) { + this._log(requestId, hookType, `Stopping: ${result.stopReason}`); + break; + } } - } - return results; + return results; + } finally { + this._onDidExecuteHook.fire({ + hookType, + sessionResource, + input: options?.input, + results, + durationMs: Math.round(sw.elapsed()), + }); + } } async executePreToolUseHook(sessionResource: URI, input: IPreToolUseCallerInput, token?: CancellationToken): Promise { - // Convert camelCase caller input to snake_case for external command const toolSpecificInput: IPreToolUseCommandInput = { tool_name: input.toolName, tool_input: input.toolInput, @@ -296,14 +339,17 @@ export class HooksExecutionService implements IHooksExecutionService { token: token ?? CancellationToken.None, }); - // Collect all valid outputs - priority order: deny > ask > allow - let lastAskResult: IPreToolUseHookResult | undefined; - let lastAllowResult: IPreToolUseHookResult | undefined; + // Run all hooks and collapse results. Most restrictive decision wins: deny > ask > allow. + // Collect all additionalContext strings from every hook. + const allAdditionalContext: string[] = []; + let mostRestrictiveDecision: PreToolUsePermissionDecision | undefined; + let winningResult: IHookResult | undefined; + let winningReason: string | undefined; + for (const result of results) { if (result.success && typeof result.output === 'object' && result.output !== null) { const validationResult = preToolUseOutputValidator.validate(result.output); if (!validationResult.error) { - // Extract from hookSpecificOutput wrapper const hookSpecificOutput = validationResult.content.hookSpecificOutput; if (hookSpecificOutput) { // Validate hookEventName if present - must match the hook type @@ -312,34 +358,116 @@ export class HooksExecutionService implements IHooksExecutionService { continue; } - const preToolUseResult: IPreToolUseHookResult = { - ...result, - permissionDecision: hookSpecificOutput.permissionDecision, - permissionDecisionReason: hookSpecificOutput.permissionDecisionReason, - additionalContext: hookSpecificOutput.additionalContext, - }; - - // If any hook denies, return immediately with that denial - if (hookSpecificOutput.permissionDecision === 'deny') { - return preToolUseResult; - } - // Track 'ask' results (ask takes priority over allow) - if (hookSpecificOutput.permissionDecision === 'ask') { - lastAskResult = preToolUseResult; + // Collect additionalContext from every hook + if (hookSpecificOutput.additionalContext) { + allAdditionalContext.push(hookSpecificOutput.additionalContext); } - // Track the last allow in case we need to return it - if (hookSpecificOutput.permissionDecision === 'allow') { - lastAllowResult = preToolUseResult; + + // Track the most restrictive decision: deny > ask > allow + const decision = hookSpecificOutput.permissionDecision; + if (decision && this._isMoreRestrictive(decision, mostRestrictiveDecision)) { + mostRestrictiveDecision = decision; + winningResult = result; + winningReason = hookSpecificOutput.permissionDecisionReason; } } } else { - // If validation fails, log a warning and continue to next result this._logService.warn(`[HooksExecutionService] preToolUse hook output validation failed: ${validationResult.error.message}`); } } } - // Return with priority: ask > allow > undefined - return lastAskResult ?? lastAllowResult; + if (!mostRestrictiveDecision || !winningResult) { + return undefined; + } + + return { + ...winningResult, + permissionDecision: mostRestrictiveDecision, + permissionDecisionReason: winningReason, + additionalContext: allAdditionalContext.length > 0 ? allAdditionalContext : undefined, + }; + } + + /** + * Returns true if `candidate` is more restrictive than `current`. + * Restriction order: deny > ask > allow. + */ + private _isMoreRestrictive(candidate: PreToolUsePermissionDecision, current: PreToolUsePermissionDecision | undefined): boolean { + const order: Record = { 'deny': 2, 'ask': 1, 'allow': 0 }; + return current === undefined || order[candidate] > order[current]; + } + + async executePostToolUseHook(sessionResource: URI, input: IPostToolUseCallerInput, token?: CancellationToken): Promise { + // Check if there are PostToolUse hooks registered before doing any work stringifying tool results + const hooks = this.getHooksForSession(sessionResource); + const hookCommands = hooks?.[HookType.PostToolUse]; + if (!hookCommands || hookCommands.length === 0) { + return undefined; + } + + // Lazily render tool response text only when hooks are registered + const toolResponseText = input.getToolResponseText(); + + const toolSpecificInput: IPostToolUseCommandInput = { + tool_name: input.toolName, + tool_input: input.toolInput, + tool_response: toolResponseText, + tool_use_id: input.toolCallId, + }; + + const results = await this.executeHook(HookType.PostToolUse, sessionResource, { + input: toolSpecificInput, + token: token ?? CancellationToken.None, + }); + + // Run all hooks and collapse results. Block is the most restrictive decision. + // Collect all additionalContext strings from every hook. + const allAdditionalContext: string[] = []; + let hasBlock = false; + let blockReason: string | undefined; + let blockResult: IHookResult | undefined; + + for (const result of results) { + if (result.success && typeof result.output === 'object' && result.output !== null) { + const validationResult = postToolUseOutputValidator.validate(result.output); + if (!validationResult.error) { + const validated = validationResult.content; + + // Validate hookEventName if present + if (validated.hookSpecificOutput?.hookEventName !== undefined && validated.hookSpecificOutput.hookEventName !== HookType.PostToolUse) { + this._logService.warn(`[HooksExecutionService] postToolUse hook returned invalid hookEventName '${validated.hookSpecificOutput.hookEventName}', expected '${HookType.PostToolUse}'`); + continue; + } + + // Collect additionalContext from every hook + if (validated.hookSpecificOutput?.additionalContext) { + allAdditionalContext.push(validated.hookSpecificOutput.additionalContext); + } + + // Track the first block decision (most restrictive) + if (validated.decision === 'block' && !hasBlock) { + hasBlock = true; + blockReason = validated.reason; + blockResult = result; + } + } else { + this._logService.warn(`[HooksExecutionService] postToolUse hook output validation failed: ${validationResult.error.message}`); + } + } + } + + // Return combined result if there's a block decision or any additional context + if (!hasBlock && allAdditionalContext.length === 0) { + return undefined; + } + + const baseResult = blockResult ?? results[0]; + return { + ...baseResult, + decision: hasBlock ? 'block' : undefined, + reason: blockReason, + additionalContext: allAdditionalContext.length > 0 ? allAdditionalContext : undefined, + }; } } diff --git a/src/vs/workbench/contrib/chat/common/hooks/hooksTypes.ts b/src/vs/workbench/contrib/chat/common/hooks/hooksTypes.ts index 8a0fb32ab783e..e562a1096c09c 100644 --- a/src/vs/workbench/contrib/chat/common/hooks/hooksTypes.ts +++ b/src/vs/workbench/contrib/chat/common/hooks/hooksTypes.ts @@ -83,12 +83,49 @@ export type PreToolUsePermissionDecision = 'allow' | 'deny' | 'ask'; /** * Result from preToolUse hooks with permission decision fields. - * Returned to VS Code callers. + * Returned to VS Code callers. Represents the collapsed result of all hooks. */ export interface IPreToolUseHookResult extends IHookResult { readonly permissionDecision?: PreToolUsePermissionDecision; readonly permissionDecisionReason?: string; - readonly additionalContext?: string; + readonly additionalContext?: string[]; +} + +//#endregion + +//#region PostToolUse Hook Types + +/** + * Input provided by VS Code callers when invoking the postToolUse hook. + * The toolResponse is a lazy getter that renders the tool result content to a string. + * It is only called if there are PostToolUse hooks registered. + */ +export interface IPostToolUseCallerInput { + readonly toolName: string; + readonly toolInput: unknown; + readonly getToolResponseText: () => string; + readonly toolCallId: string; +} + +export const postToolUseOutputValidator = vObj({ + decision: vOptionalProp(vEnum('block')), + reason: vOptionalProp(vString()), + hookSpecificOutput: vOptionalProp(vObj({ + hookEventName: vOptionalProp(vString()), + additionalContext: vOptionalProp(vString()), + })), +}); + +export type PostToolUseDecision = 'block'; + +/** + * Result from postToolUse hooks with decision fields. + * Returned to VS Code callers. Represents the collapsed result of all hooks. + */ +export interface IPostToolUseHookResult extends IHookResult { + readonly decision?: PostToolUseDecision; + readonly reason?: string; + readonly additionalContext?: string[]; } //#endregion diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 5db135db85841..e6ae43c0709ac 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -31,7 +31,7 @@ import { AGENT_MD_FILENAME, CLAUDE_LOCAL_MD_FILENAME, CLAUDE_MD_FILENAME, getCle import { PROMPT_LANGUAGE_ID, PromptsType, getPromptsTypeForLanguageId } from '../promptTypes.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js'; -import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger } from './promptsService.js'; +import { IAgentInstructions, type IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; import { IChatRequestHooks, IHookCommand, HookType } from '../hookSchema.js'; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts index 7548a6b36e295..7763b9f8b3d0e 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts @@ -9,6 +9,7 @@ import { IChatTerminalToolInvocationData } from '../../chatService/chatService.j import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../languageModelToolsService.js'; export const ConfirmationToolId = 'vscode_get_confirmation'; +export const ConfirmationToolWithOptionsId = 'vscode_get_confirmation_with_options'; export const ConfirmationToolData: IToolData = { id: ConfirmationToolId, @@ -41,11 +42,39 @@ export const ConfirmationToolData: IToolData = { } }; +export const ConfirmationToolWithOptionsData: IToolData = { + id: ConfirmationToolWithOptionsId, + displayName: 'Confirmation Tool with Options', + modelDescription: 'A tool that demonstrates different types of confirmations. Takes a title, message, and buttons.', + source: ToolDataSource.Internal, + inputSchema: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Title for the confirmation dialog' + }, + message: { + type: 'string', + description: 'Message to show in the confirmation dialog' + }, + buttons: { + type: 'array', + items: { type: 'string' }, + description: 'Custom button labels to display.' + } + }, + required: ['title', 'message', 'buttons'], + additionalProperties: false + } +}; + export interface IConfirmationToolParams { title: string; message: string; confirmationType?: 'basic' | 'terminal'; terminalCommand?: string; + buttons?: string[]; } export class ConfirmationTool implements IToolImpl { @@ -78,7 +107,8 @@ export class ConfirmationTool implements IToolImpl { confirmationMessages: { title: parameters.title, message: new MarkdownString(parameters.message), - allowAutoConfirm: true + allowAutoConfirm: (parameters.buttons || []).length ? false : true, // We cannot auto confirm if there are custom buttons, as we don't know which one to select + customButtons: parameters.buttons, }, toolSpecificData, presentation: ToolInvocationPresentation.HiddenAfterComplete @@ -86,7 +116,17 @@ export class ConfirmationTool implements IToolImpl { } async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise { - // This is a no-op tool - just return success + // If a custom button was selected, return the button label + if (invocation.selectedCustomButton) { + return { + content: [{ + kind: 'text', + value: invocation.selectedCustomButton + }] + }; + } + + // Default: return 'yes' for standard Allow confirmation return { content: [{ kind: 'text', diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index ad914109af164..d07c4263a190c 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -7,7 +7,7 @@ import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution } from '../../../../../common/contributions.js'; import { ILanguageModelToolsService } from '../languageModelToolsService.js'; -import { ConfirmationTool, ConfirmationToolData } from './confirmationTool.js'; +import { ConfirmationTool, ConfirmationToolData, ConfirmationToolWithOptionsData } from './confirmationTool.js'; import { EditTool, EditToolData } from './editFileTool.js'; import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; import { RunSubagentTool } from './runSubagentTool.js'; @@ -32,6 +32,7 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo // Register the confirmation tool const confirmationTool = instantiationService.createInstance(ConfirmationTool); this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); + this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index d8631d62bbf61..3f187cf003f91 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -182,6 +182,8 @@ export interface IToolInvocation { toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData; modelId?: string; userSelectedTools?: UserSelectedTools; + /** The label of the custom button selected by the user during confirmation, if custom buttons were used. */ + selectedCustomButton?: string; } export interface IToolInvocationContext { @@ -314,6 +316,8 @@ export interface IToolConfirmationMessages { confirmResults?: boolean; /** If title is not set (no confirmation needed), this reason will be shown to explain why confirmation was not needed */ confirmationNotNeededReason?: string | IMarkdownString; + /** Custom button labels to display instead of the default Allow/Skip buttons. */ + customButtons?: string[]; } export interface IToolConfirmationAction { diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index b212e9cb2552b..0c19f89c566de 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -8,6 +8,7 @@ import { Barrier } from '../../../../../../base/common/async.js'; import { VSBuffer } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { CancellationError, isCancellationError } from '../../../../../../base/common/errors.js'; +import { Event } from '../../../../../../base/common/event.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; @@ -32,7 +33,7 @@ import { ILanguageModelToolsConfirmationService } from '../../../common/tools/la import { MockLanguageModelToolsConfirmationService } from '../../common/tools/mockLanguageModelToolsConfirmationService.js'; import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; import { ILanguageModelChatMetadata } from '../../../common/languageModels.js'; -import { IHookResult, IPreToolUseCallerInput, IPreToolUseHookResult } from '../../../common/hooks/hooksTypes.js'; +import { IHookResult, IPostToolUseCallerInput, IPostToolUseHookResult, IPreToolUseCallerInput, IPreToolUseHookResult } from '../../../common/hooks/hooksTypes.js'; import { IHooksExecutionService, IHooksExecutionOptions, IHooksExecutionProxy } from '../../../common/hooks/hooksExecutionService.js'; import { HookTypeValue, IChatRequestHooks } from '../../../common/promptSyntax/hookSchema.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; @@ -65,8 +66,11 @@ class TestTelemetryService implements Partial { class MockHooksExecutionService implements IHooksExecutionService { readonly _serviceBrand: undefined; + readonly onDidExecuteHook = Event.None; public preToolUseHookResult: IPreToolUseHookResult | undefined = undefined; + public postToolUseHookResult: IPostToolUseHookResult | undefined = undefined; public lastPreToolUseInput: IPreToolUseCallerInput | undefined = undefined; + public lastPostToolUseInput: IPostToolUseCallerInput | undefined = undefined; setProxy(_proxy: IHooksExecutionProxy): void { } registerHooks(_sessionResource: URI, _hooks: IChatRequestHooks): IDisposable { return { dispose: () => { } }; } @@ -78,6 +82,10 @@ class MockHooksExecutionService implements IHooksExecutionService { this.lastPreToolUseInput = input; return this.preToolUseHookResult; } + async executePostToolUseHook(_sessionResource: URI, input: IPostToolUseCallerInput, _token?: CancellationToken): Promise { + this.lastPostToolUseInput = input; + return this.postToolUseHookResult; + } } function registerToolForTest(service: LanguageModelToolsService, store: any, id: string, impl: IToolImpl, data?: Partial) { @@ -522,6 +530,95 @@ suite('LanguageModelToolsService', () => { assert.strictEqual(result.content[0].value, 'ran'); }); + test('selectedCustomButton is passed to tool invoke when user selects a custom button', async () => { + let receivedInvocation: IToolInvocation | undefined; + const tool = registerToolForTest(service, store, 'testToolCustomButton', { + prepareToolInvocation: async () => ({ + confirmationMessages: { + title: 'Confirm', + message: 'Pick an option', + customButtons: ['Option A', 'Option B'], + allowAutoConfirm: false, + } + }), + invoke: async (invocation) => { + receivedInvocation = invocation; + return { content: [{ kind: 'text', value: invocation.selectedCustomButton ?? 'none' }] }; + }, + }); + + const sessionId = 'sessionId-custom-btn'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-custom-btn', capture }); + + const dto = tool.makeDto({ x: 1 }, { sessionId }); + + const promise = service.invokeTool(dto, async () => 0, CancellationToken.None); + const published = await waitForPublishedInvocation(capture); + assert.ok(published, 'expected ChatToolInvocation to be published'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction, selectedButton: 'Option A' }); + const result = await promise; + assert.strictEqual(receivedInvocation?.selectedCustomButton, 'Option A'); + assert.strictEqual(result.content[0].value, 'Option A'); + }); + + test('selectedCustomButton is not set when user confirms without custom button', async () => { + let receivedInvocation: IToolInvocation | undefined; + const tool = registerToolForTest(service, store, 'testToolNoCustomBtn', { + prepareToolInvocation: async () => ({ + confirmationMessages: { title: 'Confirm', message: 'Go?' } + }), + invoke: async (invocation) => { + receivedInvocation = invocation; + return { content: [{ kind: 'text', value: 'ok' }] }; + }, + }); + + const sessionId = 'sessionId-no-custom-btn'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-no-custom-btn', capture }); + + const dto = tool.makeDto({ x: 1 }, { sessionId }); + + const promise = service.invokeTool(dto, async () => 0, CancellationToken.None); + const published = await waitForPublishedInvocation(capture); + assert.ok(published); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(receivedInvocation?.selectedCustomButton, undefined); + assert.strictEqual(result.content[0].value, 'ok'); + }); + + test('confirmationMessages with customButtons disables allowAutoConfirm', async () => { + const tool = registerToolForTest(service, store, 'testToolCustomBtnNoAuto', { + prepareToolInvocation: async () => ({ + confirmationMessages: { + title: 'Confirm', + message: 'Choose', + customButtons: ['Yes', 'No'], + allowAutoConfirm: false, + } + }), + invoke: async () => ({ content: [{ kind: 'text', value: 'done' }] }), + }); + + const sessionId = 'sessionId-custom-noauto'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-custom-noauto', capture }); + + const dto = tool.makeDto({ x: 1 }, { sessionId }); + + const promise = service.invokeTool(dto, async () => 0, CancellationToken.None); + const published = await waitForPublishedInvocation(capture); + assert.ok(published, 'expected ChatToolInvocation to be published'); + assert.deepStrictEqual(published.confirmationMessages?.customButtons, ['Yes', 'No']); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction, selectedButton: 'Yes' }); + await promise; + }); + test('cancel tool call', async () => { const toolBarrier = new Barrier(); const tool = registerToolForTest(service, store, 'testTool', { @@ -2003,35 +2100,31 @@ suite('LanguageModelToolsService', () => { // Change the context key value contextKeyService.createKey('dynamicKey', true); - // Wait a bit for the scheduler - await new Promise(resolve => setTimeout(resolve, 800)); + service.flushToolUpdates(); assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when context keys change'); }); test('configuration changes trigger tool updates', async () => { - return runWithFakedTimers({}, async () => { - let changeEventFired = false; - const disposable = service.onDidChangeTools(() => { - changeEventFired = true; - }); - store.add(disposable); + let changeEventFired = false; + const disposable = service.onDidChangeTools(() => { + changeEventFired = true; + }); + store.add(disposable); - // Change the correct configuration key - configurationService.setUserConfiguration('chat.extensionTools.enabled', false); - // Fire the configuration change event manually - configurationService.onDidChangeConfigurationEmitter.fire({ - affectsConfiguration: () => true, - affectedKeys: new Set(['chat.extensionTools.enabled']), - change: null!, - source: ConfigurationTarget.USER - } satisfies IConfigurationChangeEvent); + // Change the correct configuration key + configurationService.setUserConfiguration('chat.extensionTools.enabled', false); + // Fire the configuration change event manually + configurationService.onDidChangeConfigurationEmitter.fire({ + affectsConfiguration: () => true, + affectedKeys: new Set(['chat.extensionTools.enabled']), + change: null!, + source: ConfigurationTarget.USER + } satisfies IConfigurationChangeEvent); - // Wait a bit for the scheduler - await new Promise(resolve => setTimeout(resolve, 800)); + service.flushToolUpdates(); - assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when configuration changes'); - }); + assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when configuration changes'); }); test('toToolAndToolSetEnablementMap with MCP toolset enables contained tools', () => { @@ -3094,47 +3187,44 @@ suite('LanguageModelToolsService', () => { configurationService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); }); - test('observeTools changes when context key changes', async () => { - return runWithFakedTimers({}, async () => { - const testCtxKey = contextKeyService.createKey('dynamicTestKey', 'value1'); + test('observeTools changes when context key changes', () => { + const testCtxKey = contextKeyService.createKey('dynamicTestKey', 'value1'); - const tool1: IToolData = { - id: 'dynamicTool1', - modelDescription: 'Dynamic Tool 1', - displayName: 'Dynamic Tool 1', - source: ToolDataSource.Internal, - when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value1'), - }; + const tool1: IToolData = { + id: 'dynamicTool1', + modelDescription: 'Dynamic Tool 1', + displayName: 'Dynamic Tool 1', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value1'), + }; - const tool2: IToolData = { - id: 'dynamicTool2', - modelDescription: 'Dynamic Tool 2', - displayName: 'Dynamic Tool 2', - source: ToolDataSource.Internal, - when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value2'), - }; + const tool2: IToolData = { + id: 'dynamicTool2', + modelDescription: 'Dynamic Tool 2', + displayName: 'Dynamic Tool 2', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value2'), + }; - store.add(service.registerToolData(tool1)); - store.add(service.registerToolData(tool2)); + store.add(service.registerToolData(tool1)); + store.add(service.registerToolData(tool2)); - const toolsObs = service.observeTools(undefined); + const toolsObs = service.observeTools(undefined); - // Initial state: value1 matches tool1 - let tools = toolsObs.get(); - assert.strictEqual(tools.length, 1, 'should have 1 tool initially'); - assert.strictEqual(tools[0].id, 'dynamicTool1', 'should be dynamicTool1'); + // Initial state: value1 matches tool1 + let tools = toolsObs.get(); + assert.strictEqual(tools.length, 1, 'should have 1 tool initially'); + assert.strictEqual(tools[0].id, 'dynamicTool1', 'should be dynamicTool1'); - // Change context key to value2 - testCtxKey.set('value2'); + // Change context key to value2 + testCtxKey.set('value2'); - // Wait for scheduler to trigger - await new Promise(resolve => setTimeout(resolve, 800)); + service.flushToolUpdates(); - // Now tool2 should be available - tools = toolsObs.get(); - assert.strictEqual(tools.length, 1, 'should have 1 tool after change'); - assert.strictEqual(tools[0].id, 'dynamicTool2', 'should be dynamicTool2 after context change'); - }); + // Now tool2 should be available + tools = toolsObs.get(); + assert.strictEqual(tools.length, 1, 'should have 1 tool after change'); + assert.strictEqual(tools[0].id, 'dynamicTool2', 'should be dynamicTool2 after context change'); }); test('isPermitted allows tools in permitted toolsets when agent mode is disabled', () => { @@ -3530,62 +3620,59 @@ suite('LanguageModelToolsService', () => { assert.ok(!toolIds.includes('toolWithWhenTrue'), 'Tool with when=true should NOT be in tool set when context key is false'); }); - test('ToolSet.getTools updates when context key changes', async () => { - return runWithFakedTimers({}, async () => { - // Create a context key for testing - const testKey = contextKeyService.createKey('dynamicTestKey', 'value1'); - - // Create tools with when clauses - const toolWithValue1: IToolData = { - id: 'toolWithValue1', - modelDescription: 'Tool with value1', - displayName: 'Tool with value1', - source: ToolDataSource.Internal, - when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value1'), - }; - - const toolWithValue2: IToolData = { - id: 'toolWithValue2', - modelDescription: 'Tool with value2', - displayName: 'Tool with value2', - source: ToolDataSource.Internal, - when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value2'), - }; - - // Create a tool set and add the tools - const dynamicToolSet = store.add(service.createToolSet( - ToolDataSource.Internal, - 'dynamicToolSet', - 'dynamicToolSetRef', - { description: 'Dynamic Tool Set' } - )); - - store.add(service.registerToolData(toolWithValue1)); - store.add(service.registerToolData(toolWithValue2)); - - store.add(dynamicToolSet.addTool(toolWithValue1)); - store.add(dynamicToolSet.addTool(toolWithValue2)); - - // Initial state: value1 is set - let tools = Array.from(dynamicToolSet.getTools()); - let toolIds = tools.map(t => t.id); - - assert.strictEqual(tools.length, 1, 'Should have 1 tool initially'); - assert.strictEqual(toolIds[0], 'toolWithValue1', 'Should be toolWithValue1'); - - // Change context key to value2 - testKey.set('value2'); - - // Wait for scheduler to trigger - await new Promise(resolve => setTimeout(resolve, 800)); - - // Now toolWithValue2 should be available - tools = Array.from(dynamicToolSet.getTools()); - toolIds = tools.map(t => t.id); - - assert.strictEqual(tools.length, 1, 'Should have 1 tool after change'); - assert.strictEqual(toolIds[0], 'toolWithValue2', 'Should be toolWithValue2 after context change'); - }); + test('ToolSet.getTools updates when context key changes', () => { + // Create a context key for testing + const testKey = contextKeyService.createKey('dynamicTestKey', 'value1'); + + // Create tools with when clauses + const toolWithValue1: IToolData = { + id: 'toolWithValue1', + modelDescription: 'Tool with value1', + displayName: 'Tool with value1', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value1'), + }; + + const toolWithValue2: IToolData = { + id: 'toolWithValue2', + modelDescription: 'Tool with value2', + displayName: 'Tool with value2', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value2'), + }; + + // Create a tool set and add the tools + const dynamicToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'dynamicToolSet', + 'dynamicToolSetRef', + { description: 'Dynamic Tool Set' } + )); + + store.add(service.registerToolData(toolWithValue1)); + store.add(service.registerToolData(toolWithValue2)); + + store.add(dynamicToolSet.addTool(toolWithValue1)); + store.add(dynamicToolSet.addTool(toolWithValue2)); + + // Initial state: value1 is set + let tools = Array.from(dynamicToolSet.getTools()); + let toolIds = tools.map(t => t.id); + + assert.strictEqual(tools.length, 1, 'Should have 1 tool initially'); + assert.strictEqual(toolIds[0], 'toolWithValue1', 'Should be toolWithValue1'); + + // Change context key to value2 + testKey.set('value2'); + + service.flushToolUpdates(); + + // Now toolWithValue2 should be available + tools = Array.from(dynamicToolSet.getTools()); + toolIds = tools.map(t => t.id); + + assert.strictEqual(tools.length, 1, 'Should have 1 tool after change'); + assert.strictEqual(toolIds[0], 'toolWithValue2', 'Should be toolWithValue2 after context change'); }); test('ToolSet.getTools with complex when expressions', () => { @@ -3898,9 +3985,7 @@ suite('LanguageModelToolsService', () => { CancellationToken.None ); - // Wait for invocation to be captured - await new Promise(resolve => setTimeout(resolve, 50)); - const invocation = capture.invocation; + const invocation = await waitForPublishedInvocation(capture); assert.ok(invocation, 'Tool invocation should be created'); // Check that the tool is waiting for confirmation (not auto-approved) @@ -3953,4 +4038,154 @@ suite('LanguageModelToolsService', () => { assert.strictEqual((result.content[0] as IToolResultTextPart).value, 'success'); }); }); + + suite('postToolUse hooks', () => { + let mockHooksService: MockHooksExecutionService; + let hookService: LanguageModelToolsService; + let hookChatService: MockChatService; + + setup(() => { + mockHooksService = new MockHooksExecutionService(); + const setup = createTestToolsService(store, { + hooksExecutionService: mockHooksService + }); + hookService = setup.service; + hookChatService = setup.chatService; + }); + + test('when hook blocks, block context is appended to tool result', async () => { + mockHooksService.postToolUseHookResult = { + output: undefined, + success: true, + decision: 'block', + reason: 'Lint errors detected', + }; + + const tool = registerToolForTest(hookService, store, 'postHookBlockTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'original output' }] }) + }); + + stubGetSession(hookChatService, 'post-hook-block', { requestId: 'req1' }); + + const result = await hookService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId: 'post-hook-block' }), + async () => 0, + CancellationToken.None + ); + + // Original content should still be present + assert.strictEqual(result.content[0].kind, 'text'); + assert.strictEqual((result.content[0] as IToolResultTextPart).value, 'original output'); + + // Block context should be appended wrapped in XML tags + assert.ok(result.content.length >= 2, 'Block context should be appended'); + const blockPart = result.content[1] as IToolResultTextPart; + assert.strictEqual(blockPart.kind, 'text'); + assert.ok(blockPart.value.includes(''), 'Block text should have opening tag'); + assert.ok(blockPart.value.includes(''), 'Block text should have closing tag'); + assert.ok(blockPart.value.includes('Lint errors detected'), 'Block text should include the reason'); + + // Should NOT set toolResultError + assert.strictEqual(result.toolResultError, undefined); + }); + + test('when hook returns additionalContext, it is appended to tool result', async () => { + mockHooksService.postToolUseHookResult = { + output: undefined, + success: true, + additionalContext: ['Consider running tests after this change'], + }; + + const tool = registerToolForTest(hookService, store, 'postHookContextTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'original output' }] }) + }); + + stubGetSession(hookChatService, 'post-hook-context', { requestId: 'req1' }); + + const result = await hookService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId: 'post-hook-context' }), + async () => 0, + CancellationToken.None + ); + + assert.strictEqual(result.content[0].kind, 'text'); + assert.strictEqual((result.content[0] as IToolResultTextPart).value, 'original output'); + + assert.ok(result.content.length >= 2, 'Additional context should be appended'); + const contextPart = result.content[1] as IToolResultTextPart; + assert.strictEqual(contextPart.kind, 'text'); + assert.ok(contextPart.value.includes(''), 'Context text should have opening tag'); + assert.ok(contextPart.value.includes(''), 'Context text should have closing tag'); + assert.ok(contextPart.value.includes('Consider running tests after this change')); + }); + + test('when hook returns undefined, tool result is unchanged', async () => { + mockHooksService.postToolUseHookResult = undefined; + + const tool = registerToolForTest(hookService, store, 'postHookNoopTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'original output' }] }) + }); + + stubGetSession(hookChatService, 'post-hook-noop', { requestId: 'req1' }); + + const result = await hookService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId: 'post-hook-noop' }), + async () => 0, + CancellationToken.None + ); + + assert.strictEqual(result.content.length, 1); + assert.strictEqual(result.content[0].kind, 'text'); + assert.strictEqual((result.content[0] as IToolResultTextPart).value, 'original output'); + }); + + test('hook receives correct input including tool response text', async () => { + mockHooksService.postToolUseHookResult = undefined; + + const tool = registerToolForTest(hookService, store, 'postHookInputTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'file contents here' }] }) + }); + + stubGetSession(hookChatService, 'post-hook-input', { requestId: 'req1' }); + + await hookService.invokeTool( + tool.makeDto({ param1: 'value1' }, { sessionId: 'post-hook-input' }), + async () => 0, + CancellationToken.None + ); + + assert.ok(mockHooksService.lastPostToolUseInput); + assert.strictEqual(mockHooksService.lastPostToolUseInput.toolName, 'postHookInputTool'); + assert.deepStrictEqual(mockHooksService.lastPostToolUseInput.toolInput, { param1: 'value1' }); + assert.strictEqual(typeof mockHooksService.lastPostToolUseInput.getToolResponseText, 'function'); + }); + + test('when hook blocks with both decision and additionalContext, both are appended', async () => { + mockHooksService.postToolUseHookResult = { + output: undefined, + success: true, + decision: 'block', + reason: 'Security issue found', + additionalContext: ['Please review the file permissions'], + }; + + const tool = registerToolForTest(hookService, store, 'postHookBlockContextTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'original' }] }) + }); + + stubGetSession(hookChatService, 'post-hook-block-ctx', { requestId: 'req1' }); + + const result = await hookService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId: 'post-hook-block-ctx' }), + async () => 0, + CancellationToken.None + ); + + // Original + block message + additional context = 3 parts + assert.ok(result.content.length >= 3, 'Should have original, block message, and additional context'); + assert.strictEqual((result.content[0] as IToolResultTextPart).value, 'original'); + assert.ok((result.content[1] as IToolResultTextPart).value.includes('Security issue found')); + assert.ok((result.content[2] as IToolResultTextPart).value.includes('Please review the file permissions')); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/hooksExecutionService.test.ts b/src/vs/workbench/contrib/chat/test/common/hooksExecutionService.test.ts index fa4aae5de8eb3..c50fc0efda40c 100644 --- a/src/vs/workbench/contrib/chat/test/common/hooksExecutionService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/hooksExecutionService.test.ts @@ -34,7 +34,7 @@ suite('HooksExecutionService', () => { const sessionUri = URI.file('/test/session'); setup(() => { - service = new HooksExecutionService(new NullLogService(), createMockOutputService()); + service = store.add(new HooksExecutionService(new NullLogService(), createMockOutputService())); }); suite('registerHooks', () => { @@ -496,6 +496,391 @@ suite('HooksExecutionService', () => { }); }); + suite('executePostToolUseHook', () => { + test('returns undefined when no hooks configured', async () => { + const proxy = createMockProxy(); + service.setProxy(proxy); + + const hooks = { [HookType.PreToolUse]: [cmd('hook')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const result = await service.executePostToolUseHook( + sessionUri, + { toolName: 'test-tool', toolInput: {}, getToolResponseText: () => 'tool output', toolCallId: 'call-1' } + ); + + assert.strictEqual(result, undefined); + }); + + test('returns block decision when hook blocks', async () => { + const proxy = createMockProxy(() => ({ + kind: HookCommandResultKind.Success, + result: { + decision: 'block', + reason: 'Lint errors found' + } + })); + service.setProxy(proxy); + + const hooks = { [HookType.PostToolUse]: [cmd('hook')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const result = await service.executePostToolUseHook( + sessionUri, + { toolName: 'test-tool', toolInput: {}, getToolResponseText: () => 'tool output', toolCallId: 'call-1' } + ); + + assert.ok(result); + assert.strictEqual(result.decision, 'block'); + assert.strictEqual(result.reason, 'Lint errors found'); + }); + + test('returns additionalContext from hookSpecificOutput', async () => { + const proxy = createMockProxy(() => ({ + kind: HookCommandResultKind.Success, + result: { + hookSpecificOutput: { + hookEventName: 'PostToolUse', + additionalContext: 'File was modified successfully' + } + } + })); + service.setProxy(proxy); + + const hooks = { [HookType.PostToolUse]: [cmd('hook')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const result = await service.executePostToolUseHook( + sessionUri, + { toolName: 'test-tool', toolInput: {}, getToolResponseText: () => 'tool output', toolCallId: 'call-1' } + ); + + assert.ok(result); + assert.deepStrictEqual(result.additionalContext, ['File was modified successfully']); + assert.strictEqual(result.decision, undefined); + }); + + test('block takes priority and collects all additionalContext', async () => { + let callCount = 0; + const proxy = createMockProxy(() => { + callCount++; + if (callCount === 1) { + return { + kind: HookCommandResultKind.Success, + result: { + decision: 'block', + reason: 'Tests failed' + } + }; + } else { + return { + kind: HookCommandResultKind.Success, + result: { + hookSpecificOutput: { + additionalContext: 'Extra context from second hook' + } + } + }; + } + }); + service.setProxy(proxy); + + const hooks = { [HookType.PostToolUse]: [cmd('hook1'), cmd('hook2')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const result = await service.executePostToolUseHook( + sessionUri, + { toolName: 'test-tool', toolInput: {}, getToolResponseText: () => 'tool output', toolCallId: 'call-1' } + ); + + assert.ok(result); + assert.strictEqual(result.decision, 'block'); + assert.strictEqual(result.reason, 'Tests failed'); + assert.deepStrictEqual(result.additionalContext, ['Extra context from second hook']); + }); + + test('ignores results with wrong hookEventName', async () => { + let callCount = 0; + const proxy = createMockProxy(() => { + callCount++; + if (callCount === 1) { + return { + kind: HookCommandResultKind.Success, + result: { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Should be ignored' + } + } + }; + } else { + return { + kind: HookCommandResultKind.Success, + result: { + hookSpecificOutput: { + hookEventName: 'PostToolUse', + additionalContext: 'Correct context' + } + } + }; + } + }); + service.setProxy(proxy); + + const hooks = { [HookType.PostToolUse]: [cmd('hook1'), cmd('hook2')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const result = await service.executePostToolUseHook( + sessionUri, + { toolName: 'test-tool', toolInput: {}, getToolResponseText: () => 'tool output', toolCallId: 'call-1' } + ); + + assert.ok(result); + assert.deepStrictEqual(result.additionalContext, ['Correct context']); + }); + + test('passes tool response text as string to external command', async () => { + let receivedInput: unknown; + const proxy = createMockProxy((_cmd, input) => { + receivedInput = input; + return { kind: HookCommandResultKind.Success, result: {} }; + }); + service.setProxy(proxy); + + const hooks = { [HookType.PostToolUse]: [cmd('hook')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + await service.executePostToolUseHook( + sessionUri, + { toolName: 'my-tool', toolInput: { arg: 'val' }, getToolResponseText: () => 'file contents here', toolCallId: 'call-42' } + ); + + assert.ok(typeof receivedInput === 'object' && receivedInput !== null); + const input = receivedInput as Record; + assert.strictEqual(input['tool_name'], 'my-tool'); + assert.deepStrictEqual(input['tool_input'], { arg: 'val' }); + assert.strictEqual(input['tool_response'], 'file contents here'); + assert.strictEqual(input['tool_use_id'], 'call-42'); + assert.strictEqual(input['hookEventName'], HookType.PostToolUse); + }); + + test('does not call getter when no PostToolUse hooks registered', async () => { + const proxy = createMockProxy(); + service.setProxy(proxy); + + // Register hooks only for PreToolUse, not PostToolUse + const hooks = { [HookType.PreToolUse]: [cmd('hook')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + let getterCalled = false; + const result = await service.executePostToolUseHook( + sessionUri, + { + toolName: 'test-tool', + toolInput: {}, + getToolResponseText: () => { getterCalled = true; return ''; }, + toolCallId: 'call-1' + } + ); + + assert.strictEqual(result, undefined); + assert.strictEqual(getterCalled, false); + }); + }); + + suite('preToolUse smoke tests — input → output', () => { + test('single hook: allow', async () => { + const proxy = createMockProxy(() => ({ + kind: HookCommandResultKind.Success, + result: { + hookSpecificOutput: { + permissionDecision: 'allow', + permissionDecisionReason: 'Trusted tool', + } + } + })); + service.setProxy(proxy); + + const hooks = { [HookType.PreToolUse]: [cmd('lint-check')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const input = { toolName: 'readFile', toolInput: { path: '/src/index.ts' }, toolCallId: 'call-1' }; + const result = await service.executePreToolUseHook(sessionUri, input); + + assert.deepStrictEqual( + JSON.stringify({ permissionDecision: result?.permissionDecision, permissionDecisionReason: result?.permissionDecisionReason, additionalContext: result?.additionalContext }), + JSON.stringify({ permissionDecision: 'allow', permissionDecisionReason: 'Trusted tool', additionalContext: undefined }) + ); + }); + + test('single hook: deny', async () => { + const proxy = createMockProxy(() => ({ + kind: HookCommandResultKind.Success, + result: { + hookSpecificOutput: { + permissionDecision: 'deny', + permissionDecisionReason: 'Path is outside workspace', + } + } + })); + service.setProxy(proxy); + + const hooks = { [HookType.PreToolUse]: [cmd('path-guard')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const input = { toolName: 'writeFile', toolInput: { path: '/etc/passwd' }, toolCallId: 'call-2' }; + const result = await service.executePreToolUseHook(sessionUri, input); + + assert.deepStrictEqual( + JSON.stringify({ permissionDecision: result?.permissionDecision, permissionDecisionReason: result?.permissionDecisionReason }), + JSON.stringify({ permissionDecision: 'deny', permissionDecisionReason: 'Path is outside workspace' }) + ); + }); + + test('multiple hooks: deny wins over allow and ask', async () => { + // Three hooks return allow, ask, deny (in that order). + // deny must win regardless of ordering. + let callCount = 0; + const decisions = ['allow', 'ask', 'deny'] as const; + const proxy = createMockProxy(() => { + const decision = decisions[callCount++]; + return { + kind: HookCommandResultKind.Success, + result: { hookSpecificOutput: { permissionDecision: decision, permissionDecisionReason: `hook-${callCount}` } } + }; + }); + service.setProxy(proxy); + + const hooks = { [HookType.PreToolUse]: [cmd('h1'), cmd('h2'), cmd('h3')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const result = await service.executePreToolUseHook( + sessionUri, + { toolName: 'runCommand', toolInput: { cmd: 'rm -rf /' }, toolCallId: 'call-3' } + ); + + assert.deepStrictEqual( + JSON.stringify({ permissionDecision: result?.permissionDecision, permissionDecisionReason: result?.permissionDecisionReason }), + JSON.stringify({ permissionDecision: 'deny', permissionDecisionReason: 'hook-3' }) + ); + }); + + test('multiple hooks: ask wins over allow', async () => { + let callCount = 0; + const decisions = ['allow', 'ask'] as const; + const proxy = createMockProxy(() => { + const decision = decisions[callCount++]; + return { + kind: HookCommandResultKind.Success, + result: { hookSpecificOutput: { permissionDecision: decision, permissionDecisionReason: `reason-${decision}` } } + }; + }); + service.setProxy(proxy); + + const hooks = { [HookType.PreToolUse]: [cmd('h1'), cmd('h2')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const result = await service.executePreToolUseHook( + sessionUri, + { toolName: 'exec', toolInput: {}, toolCallId: 'call-4' } + ); + + assert.deepStrictEqual( + JSON.stringify({ permissionDecision: result?.permissionDecision, permissionDecisionReason: result?.permissionDecisionReason }), + JSON.stringify({ permissionDecision: 'ask', permissionDecisionReason: 'reason-ask' }) + ); + }); + }); + + suite('postToolUse smoke tests — input → output', () => { + test('single hook: block', async () => { + const proxy = createMockProxy(() => ({ + kind: HookCommandResultKind.Success, + result: { + decision: 'block', + reason: 'Lint errors found' + } + })); + service.setProxy(proxy); + + const hooks = { [HookType.PostToolUse]: [cmd('lint')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const input = { toolName: 'writeFile', toolInput: { path: 'foo.ts' }, getToolResponseText: () => 'wrote 42 bytes', toolCallId: 'call-5' }; + const result = await service.executePostToolUseHook(sessionUri, input); + + assert.deepStrictEqual( + JSON.stringify({ decision: result?.decision, reason: result?.reason, additionalContext: result?.additionalContext }), + JSON.stringify({ decision: 'block', reason: 'Lint errors found', additionalContext: undefined }) + ); + }); + + test('single hook: additionalContext only', async () => { + const proxy = createMockProxy(() => ({ + kind: HookCommandResultKind.Success, + result: { + hookSpecificOutput: { + additionalContext: 'Tests still pass after this edit' + } + } + })); + service.setProxy(proxy); + + const hooks = { [HookType.PostToolUse]: [cmd('test-runner')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const input = { toolName: 'editFile', toolInput: {}, getToolResponseText: () => 'ok', toolCallId: 'call-6' }; + const result = await service.executePostToolUseHook(sessionUri, input); + + assert.deepStrictEqual( + JSON.stringify({ decision: result?.decision, reason: result?.reason, additionalContext: result?.additionalContext }), + JSON.stringify({ decision: undefined, reason: undefined, additionalContext: ['Tests still pass after this edit'] }) + ); + }); + + test('multiple hooks: block wins and all hooks run', async () => { + let callCount = 0; + const proxy = createMockProxy(() => { + callCount++; + if (callCount === 1) { + return { kind: HookCommandResultKind.Success, result: { decision: 'block', reason: 'Tests failed' } }; + } + return { kind: HookCommandResultKind.Success, result: { hookSpecificOutput: { additionalContext: 'context from second hook' } } }; + }); + service.setProxy(proxy); + + const hooks = { [HookType.PostToolUse]: [cmd('test'), cmd('lint')] }; + store.add(service.registerHooks(sessionUri, hooks)); + + const result = await service.executePostToolUseHook( + sessionUri, + { toolName: 'writeFile', toolInput: {}, getToolResponseText: () => 'data', toolCallId: 'call-7' } + ); + + assert.deepStrictEqual( + JSON.stringify({ decision: result?.decision, reason: result?.reason, additionalContext: result?.additionalContext }), + JSON.stringify({ decision: 'block', reason: 'Tests failed', additionalContext: ['context from second hook'] }) + ); + }); + + test('no hooks registered → undefined (getter never called)', async () => { + const proxy = createMockProxy(); + service.setProxy(proxy); + + // Register PreToolUse only — no PostToolUse + store.add(service.registerHooks(sessionUri, { [HookType.PreToolUse]: [cmd('h')] })); + + let getterCalled = false; + const result = await service.executePostToolUseHook( + sessionUri, + { toolName: 't', toolInput: {}, getToolResponseText: () => { getterCalled = true; return ''; }, toolCallId: 'c' } + ); + + assert.strictEqual(result, undefined); + assert.strictEqual(getterCalled, false); + }); + }); + function createMockProxy(handler?: (cmd: IHookCommand, input: unknown, token: CancellationToken) => IHookCommandResult): IHooksExecutionProxy { return { runHookCommand: async (hookCommand, input, token) => { diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 475634f67c2e5..2360430ce9644 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -8,7 +8,7 @@ import { coalesce } from '../../../../base/common/arrays.js'; import { findFirstIdxMonotonousOrArrLen } from '../../../../base/common/arraysFind.js'; import { CancelablePromise, createCancelablePromise, Delayer } from '../../../../base/common/async.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; import './media/review.css'; import { ICodeEditor, IEditorMouseEvent, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; @@ -448,9 +448,8 @@ export function revealCommentThread(commentService: ICommentService, editorServi }); } -export class CommentController implements IEditorContribution { - private readonly globalToDispose = new DisposableStore(); - private readonly localToDispose = new DisposableStore(); +export class CommentController extends Disposable implements IEditorContribution { + private readonly localToDispose: DisposableStore = this._register(new DisposableStore()); private editor: ICodeEditor | undefined; private _commentWidgets: ReviewZoneWidget[]; private _commentInfos: ICommentInfo[]; @@ -489,6 +488,7 @@ export class CommentController implements IEditorContribution { @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @INotificationService private readonly notificationService: INotificationService ) { + super(); this._commentInfos = []; this._commentWidgets = []; this._pendingNewCommentCache = {}; @@ -506,7 +506,7 @@ export class CommentController implements IEditorContribution { this.editor = editor; this._commentingRangeDecorator = new CommentingRangeDecorator(); - this.globalToDispose.add(this._commentingRangeDecorator.onDidChangeDecorationsCount(count => { + this._register(this._commentingRangeDecorator.onDidChangeDecorationsCount(count => { if (count === 0) { this.clearEditorListeners(); } else if (this._editorDisposables.length === 0) { @@ -514,9 +514,9 @@ export class CommentController implements IEditorContribution { } })); - this.globalToDispose.add(this._commentThreadRangeDecorator = new CommentThreadRangeDecorator(this.commentService)); + this._register(this._commentThreadRangeDecorator = new CommentThreadRangeDecorator(this.commentService)); - this.globalToDispose.add(this.commentService.onDidDeleteDataProvider(ownerId => { + this._register(this.commentService.onDidDeleteDataProvider(ownerId => { if (ownerId) { delete this._pendingNewCommentCache[ownerId]; delete this._pendingEditsCache[ownerId]; @@ -526,17 +526,17 @@ export class CommentController implements IEditorContribution { } this.beginCompute(); })); - this.globalToDispose.add(this.commentService.onDidSetDataProvider(_ => this.beginComputeAndHandleEditorChange())); - this.globalToDispose.add(this.commentService.onDidUpdateCommentingRanges(_ => this.beginComputeAndHandleEditorChange())); + this._register(this.commentService.onDidSetDataProvider(_ => this.beginComputeAndHandleEditorChange())); + this._register(this.commentService.onDidUpdateCommentingRanges(_ => this.beginComputeAndHandleEditorChange())); - this.globalToDispose.add(this.commentService.onDidSetResourceCommentInfos(async e => { + this._register(this.commentService.onDidSetResourceCommentInfos(async e => { const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri; if (editorURI && editorURI.toString() === e.resource.toString()) { await this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null)); } })); - this.globalToDispose.add(this.commentService.onDidChangeCommentingEnabled(e => { + this._register(this.commentService.onDidChangeCommentingEnabled(e => { if (e) { this.registerEditorListeners(); this.beginCompute(); @@ -550,17 +550,17 @@ export class CommentController implements IEditorContribution { } })); - this.globalToDispose.add(this.editor.onWillChangeModel(e => this.onWillChangeModel(e))); - this.globalToDispose.add(this.editor.onDidChangeModel(_ => this.onModelChanged())); - this.globalToDispose.add(this.configurationService.onDidChangeConfiguration(e => { + this._register(this.editor.onWillChangeModel(e => this.onWillChangeModel(e))); + this._register(this.editor.onDidChangeModel(_ => this.onModelChanged())); + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('diffEditor.renderSideBySide')) { this.beginCompute(); } })); this.onModelChanged(); - this.globalToDispose.add(this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {})); - this.globalToDispose.add( + this._register(this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {})); + this._register( this.commentService.registerContinueOnCommentProvider({ provideContinueOnComments: () => { const pendingComments: languages.PendingCommentThread[] = []; @@ -879,9 +879,8 @@ export class CommentController implements IEditorContribution { this._findNearestCommentingRange(true); } - public dispose(): void { - this.globalToDispose.dispose(); - this.localToDispose.dispose(); + public override dispose(): void { + super.dispose(); dispose(this._editorDisposables); dispose(this._commentWidgets); @@ -1098,8 +1097,8 @@ export class CommentController implements IEditorContribution { const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.comment, pendingEdits); await zoneWidget.display(thread.range, shouldReveal); this._commentWidgets.push(zoneWidget); - zoneWidget.onDidChangeExpandedState(() => this._updateCommentWidgetVisibleContext()); - zoneWidget.onDidClose(() => this._updateCommentWidgetVisibleContext()); + this.localToDispose.add(zoneWidget.onDidChangeExpandedState(() => this._updateCommentWidgetVisibleContext())); + this.localToDispose.add(zoneWidget.onDidClose(() => this._updateCommentWidgetVisibleContext())); this.openCommentsView(thread); } diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index 7be817e359206..39ad37bd83ae6 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -39,6 +39,7 @@ import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js'; import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; +import { IUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js'; export const ctxCommentEditorFocused = new RawContextKey('commentEditorFocused', false); export const MIN_EDITOR_HEIGHT = 5 * 18; @@ -66,6 +67,7 @@ export class SimpleCommentEditor extends CodeEditorWidget { @IAccessibilityService accessibilityService: IAccessibilityService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IUserInteractionService userInteractionService: IUserInteractionService, ) { const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { contributions: [ @@ -91,7 +93,7 @@ export class SimpleCommentEditor extends CodeEditorWidget { contextMenuId: MenuId.SimpleEditorContext }; - super(domElement, options, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, scopedContextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); + super(domElement, options, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, scopedContextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService, userInteractionService); this._commentEditorFocused = ctxCommentEditorFocused.bindTo(scopedContextKeyService); this._commentEditorEmpty = CommentContextKeys.commentIsEmpty.bindTo(scopedContextKeyService); diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 522fbd58a7131..43c4dba2f34b6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -308,8 +308,11 @@ export class FocusSessionActionViewItem extends SelectActionViewItem dispose(sessionListeners))); this.update(); })); + // Apply the same pattern to existing sessions - track listeners for cleanup this.getSessions().forEach(session => { - this._register(session.onDidChangeName(() => this.update())); + const sessionListeners: IDisposable[] = []; + sessionListeners.push(session.onDidChangeName(() => this.update())); + sessionListeners.push(session.onDidEndAdapter(() => dispose(sessionListeners))); }); this._register(this.debugService.onDidEndSession(() => this.update())); diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 9374887baddf9..57d4ad3e8c4ca 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -91,7 +91,7 @@ export class DebugTaskRunner implements IDisposable { return Promise.resolve(TaskRunResult.Failure); } - const taskLabel = typeof taskId === 'string' ? taskId : taskId ? taskId.name : ''; + const taskLabel = typeof taskId === 'string' ? taskId : taskId ? taskId.name as string : ''; const message = errorCount > 1 ? nls.localize('preLaunchTaskErrors', "Errors exist after running preLaunchTask '{0}'.", taskLabel) : errorCount === 1 diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index bc515386dd85f..531c1146f9728 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -13,7 +13,7 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { createMatches, FuzzyScore } from '../../../../base/common/filters.js'; import { normalizeDriveLetter, tildify } from '../../../../base/common/labels.js'; -import { dispose } from '../../../../base/common/lifecycle.js'; +import { dispose, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; import { isAbsolute, normalize, posix } from '../../../../base/common/path.js'; import { isWindows } from '../../../../base/common/platform.js'; import { ltrim } from '../../../../base/common/strings.js'; @@ -541,15 +541,21 @@ export class LoadedScriptsView extends ViewPane { } }; + // Track listeners per session to avoid leaking disposables + const sessionListeners = this._register(new DisposableMap()); + const registerSessionListeners = (session: IDebugSession) => { - this._register(session.onDidChangeName(async () => { + const store = new DisposableStore(); + sessionListeners.set(session.getId(), store); + + store.add(session.onDidChangeName(async () => { const sessionRoot = root.find(session); if (sessionRoot) { sessionRoot.updateLabel(session.getLabel()); scheduleRefreshOnVisible(); } })); - this._register(session.onDidLoadedSource(async event => { + store.add(session.onDidLoadedSource(async event => { let sessionRoot: SessionTreeItem; switch (event.reason) { case 'new': @@ -579,6 +585,7 @@ export class LoadedScriptsView extends ViewPane { this.debugService.getModel().getSessions().forEach(registerSessionListeners); this._register(this.debugService.onDidEndSession(({ session }) => { + sessionListeners.deleteAndDispose(session.getId()); root.remove(session.getId()); this.changeScheduler.schedule(); })); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 4af65c2deba85..23df275636108 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -335,6 +335,7 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(ClearLanguageAction), this.instantiationService.createInstance(EnableDropDownAction), + this.instantiationService.createInstance(TogglePreReleaseExtensionAction), this.instantiationService.createInstance(DisableDropDownAction), this.instantiationService.createInstance(RemoteInstallAction, false), this.instantiationService.createInstance(LocalInstallAction), @@ -348,7 +349,6 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(InstallAnotherVersionAction, null, true), ] ]), - this.instantiationService.createInstance(TogglePreReleaseExtensionAction), this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction), new ExtensionEditorManageExtensionAction(this.scopedContextKeyService || this.contextKeyService, this.instantiationService), ]; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index f2a1f70f54f27..7dad5df8d18e3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -29,7 +29,7 @@ import { CommandsRegistry, ICommandService } from '../../../../platform/commands import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { buttonBackground, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, buttonSecondaryBorder, registerColor, editorWarningForeground, editorInfoForeground, editorErrorForeground, buttonSeparator } from '../../../../platform/theme/common/colorRegistry.js'; +import { buttonBackground, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, registerColor, editorWarningForeground, editorInfoForeground, editorErrorForeground, buttonSeparator, buttonBorder, contrastBorder } from '../../../../platform/theme/common/colorRegistry.js'; import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js'; import { ITextEditorSelection } from '../../../../platform/editor/common/editor.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; @@ -1793,7 +1793,7 @@ export class DisableDropDownAction extends ButtonWithDropDownExtensionAction { export class ExtensionRuntimeStateAction extends ExtensionAction { - private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} reload`; + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} prominent reload`; private static readonly DisabledClass = `${this.EnabledClass} disabled`; updateWhenCounterExtensionChanges: boolean = true; @@ -3197,10 +3197,10 @@ registerColor('extensionButton.hoverBackground', { }, localize('extensionButtonHoverBackground', "Button background hover color for extension actions.")); registerColor('extensionButton.border', { - dark: buttonSecondaryBorder, - light: buttonSecondaryBorder, - hcDark: null, - hcLight: null + dark: buttonBorder, + light: buttonBorder, + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('extensionButtonBorder', "Button border color for extension actions.")); registerColor('extensionButton.separator', buttonSeparator, localize('extensionButtonSeparator', "Button separator color for extension actions")); diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 0ba9d0381a8a3..d581283187e26 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -203,7 +203,7 @@ } .extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .action-label:not(.icon) { - border-radius: 2px; + border-radius: var(--vscode-cornerRadius-small); } .extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .extension-action.label { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 3b3be83e50efa..26c3a60a33908 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -17,6 +17,7 @@ import { HoverService } from '../../../../platform/hover/browser/hoverService.js import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js'; import { ACTION_START } from '../common/inlineChat.js'; export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { @@ -30,6 +31,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { @IInstantiationService instantiationService: IInstantiationService, @IAccessibilityService accessibilityService: IAccessibilityService, @IThemeService themeService: IThemeService, + @IUserInteractionService userInteractionService: IUserInteractionService, ) { const data = derived(r => { const value = selection.read(r); @@ -66,7 +68,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { super( _myEditorObs, data, constObservable(InlineEditTabAction.Inactive), constObservable(0), constObservable(false), focusIsInMenu, - hoverService, instantiationService, accessibilityService, themeService + hoverService, instantiationService, accessibilityService, themeService, userInteractionService ); this._store.add(autorun(r => { diff --git a/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts b/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts index 45af71b0fb46c..b046a750746a0 100644 --- a/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts +++ b/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts @@ -11,6 +11,7 @@ import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; import { localize } from '../../../../nls.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ILanguageStatusService } from '../../../services/languageStatus/common/languageStatusService.js'; @@ -22,15 +23,18 @@ export class InlineCompletionLanguageStatusBarContribution extends Disposable im private _activeEditor; private _state; + private _sentiment; constructor( @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, @IEditorService private readonly _editorService: IEditorService, + @IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService, ) { super(); this._activeEditor = observableFromEvent(this, _editorService.onDidActiveEditorChange, () => this._editorService.activeTextEditorControl); + this._sentiment = this._chatEntitlementService.sentimentObs; this._state = derived(this, reader => { const editor = this._activeEditor.read(reader); if (!editor || !isCodeEditor(editor)) { @@ -50,6 +54,12 @@ export class InlineCompletionLanguageStatusBarContribution extends Disposable im }); this._register(autorunWithStore((reader, store) => { + // Do not show the Copilot icon in the language status when AI features are disabled + const sentiment = this._sentiment.read(reader); + if (sentiment.hidden) { + return; + } + const state = this._state.read(reader); if (!state) { return; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 9bd44961577fe..2c59b8933266c 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -95,6 +95,13 @@ flex: auto; } +.settings-editor > .settings-header > .settings-header-controls > .settings-right-controls { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 4px; +} + .settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label { opacity: 0.9; border-radius: 0; @@ -118,6 +125,7 @@ opacity: 1; color: var(--vscode-settings-headerForeground); } + .settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label.checked:not(:focus) { border-bottom-color: var(--vscode-settings-headerForeground); } @@ -173,7 +181,8 @@ } .settings-editor > .settings-body > .monaco-split-view2.separator-border .split-view-view:not(:first-child):before { - z-index: 16; /* Above sticky scroll */ + z-index: 16; + /* Above sticky scroll */ } .settings-editor > .settings-body .settings-toc-container, @@ -228,7 +237,8 @@ } .settings-editor > .settings-body .settings-tree-container .monaco-tree-sticky-container .monaco-list-row.focused .settings-row-inner-container { - background-color: unset; /* Remove Sticky Scroll focus */ + background-color: unset; + /* Remove Sticky Scroll focus */ } .settings-editor > .settings-body .settings-tree-container .monaco-list-row:not(.focused) .settings-row-inner-container:hover { @@ -241,7 +251,8 @@ } .settings-editor > .settings-body .settings-tree-container .monaco-list:focus-within .monaco-tree-sticky-container .monaco-list-row.focused .settings-group-title-label { - outline: none; /* Remove Sticky Scroll focus */ + outline: none; + /* Remove Sticky Scroll focus */ } .settings-editor > .settings-body .settings-tree-container .settings-editor-tree > .monaco-scrollable-element > .shadow.top { @@ -336,6 +347,7 @@ .settings-editor > .settings-body .settings-tree-container .monaco-scrollable-element { padding-top: 0px; } + .settings-editor > .settings-body .settings-toc-container .monaco-scrollable-element { padding-top: 0px; } @@ -363,20 +375,25 @@ } .settings-editor > .settings-body .settings-tree-container .monaco-list-rows { - min-height: 100%; /* Avoid the hover being cut off. See #164602 and #165518 */ - overflow: visible !important; /* Allow validation errors to flow out of the tree container. Override inline style from ScrollableElement. */ + min-height: 100%; + /* Avoid the hover being cut off. See #164602 and #165518 */ + overflow: visible !important; + /* Allow validation errors to flow out of the tree container. Override inline style from ScrollableElement. */ } .settings-editor > .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents { - max-width: min(100%, 1200px); /* We don't want the widgets to be too long */ + max-width: min(100%, 1200px); + /* We don't want the widgets to be too long */ margin: auto; box-sizing: border-box; padding-left: 24px; padding-right: 24px; overflow: visible; } + .settings-editor > .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents.group-title { - max-width: min(100%, 1200px); /* Cut off title if too long for window */ + max-width: min(100%, 1200px); + /* Cut off title if too long for window */ } .settings-editor > .settings-body .settings-tree-container .settings-group-title-label, @@ -393,8 +410,10 @@ .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title { overflow: hidden; text-overflow: ellipsis; - display: inline-block; /* size to contents for hover to show context button */ - padding-bottom: 2px; /* so that focus outlines wrap around nicely for indicators, especially ones with codicons */ + display: inline-block; + /* size to contents for hover to show context button */ + padding-bottom: 2px; + /* so that focus outlines wrap around nicely for indicators, especially ones with codicons */ } .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-modified-indicator { @@ -517,6 +536,7 @@ margin-top: -1px; z-index: 1; } + .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-item-contents.invalid-input .setting-item-validation-message { position: static; margin-top: 1rem; @@ -544,6 +564,7 @@ .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown * { margin: 0px; } + .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown *:not(:last-child) { margin-bottom: 8px; } @@ -727,12 +748,15 @@ overflow: hidden; text-overflow: ellipsis; } + .settings-editor > .settings-body .settings-tree-container .settings-group-title-label.settings-group-level-1 { font-size: 26px; } + .settings-editor > .settings-body .settings-tree-container .settings-group-title-label.settings-group-level-2 { font-size: 22px; } + .settings-editor > .settings-body .settings-tree-container .settings-group-title-label.settings-group-level-3 { font-size: 18px; } @@ -744,6 +768,7 @@ .settings-editor > .settings-body .settings-tree-container .setting-list-widget .setting-list-object-list-row.select-container { width: 320px; } + .settings-editor > .settings-body .settings-tree-container .setting-list-widget .setting-list-object-list-row.select-container > select { width: inherit; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index be6384268ca1c..c1bd907895e4c 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -248,7 +248,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor, args: string | IOpenSettingsActionOptions) { // args takes a string for backcompat const opts = typeof args === 'string' ? { query: args } : sanitizeOpenSettingsArgs(args); - return accessor.get(IPreferencesService).openSettings({ openInModal: true, ...opts }); + return accessor.get(IPreferencesService).openSettings({ ...opts }); } })); this._register(registerAction2(class extends Action2 { @@ -846,7 +846,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor, ...args: unknown[]) { const query = typeof args[0] === 'string' ? args[0] : undefined; const groupId = getEditorGroupFromArguments(accessor, args)?.id; - return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query, groupId, openInModal: true }); + return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query, groupId }); } })); this._register(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 5f8db4b627d60..0c8da5089e1bc 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -787,8 +787,15 @@ export class SettingsEditor2 extends EditorPane { } })); + const headerRightControlsContainer = DOM.append(headerControlsContainer, $('.settings-right-controls')); + + const openSettingsJsonContainer = DOM.append(headerRightControlsContainer, $('.open-settings-json')); + const openSettingsJsonButton = this._register(new Button(openSettingsJsonContainer, { secondary: true, title: true, ...defaultButtonStyles })); + openSettingsJsonButton.label = localize('openSettingsJson', "Edit as JSON"); + this._register(openSettingsJsonButton.onDidClick(() => this.openSettingsFile())); + if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) { - const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerControlsContainer)); + const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerRightControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => { this.lastSyncedLabel = lastSyncedLabel; this.updateInputAriaLabel(); @@ -2280,10 +2287,9 @@ class SyncControls extends Disposable { ) { super(); - const headerRightControlsContainer = DOM.append(container, $('.settings-right-controls')); - const turnOnSyncButtonContainer = DOM.append(headerRightControlsContainer, $('.turn-on-sync')); + const turnOnSyncButtonContainer = DOM.append(container, $('.turn-on-sync')); this.turnOnSyncButton = this._register(new Button(turnOnSyncButtonContainer, { title: true, ...defaultButtonStyles })); - this.lastSyncedLabel = DOM.append(headerRightControlsContainer, $('.last-synced-label')); + this.lastSyncedLabel = DOM.append(container, $('.last-synced-label')); DOM.hide(this.lastSyncedLabel); this.turnOnSyncButton.enabled = true; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index fbd06c80137bb..8431ea8b74d4e 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -488,6 +488,7 @@ export async function createTocTreeForExtensionSettings(extensionService: IExten const processGroupEntry = async (group: ISettingsGroup) => { const flatSettings = group.sections.map(section => section.settings).flat(); const settings = filter ? getMatchingSettings(new Set(flatSettings), filter) : flatSettings; + sortSettings(settings); const extensionId = group.extensionInfo!.id; const extension = await extensionService.getExtension(extensionId); @@ -575,6 +576,7 @@ function _resolveSettingsTree(tocData: ITOCEntry, allSettings: Set, allSettings: Set { + if (setting.tags?.includes('experimental')) { + return SETTING_STATUS_EXPERIMENTAL; + } else if (setting.tags?.includes('preview')) { + return SETTING_STATUS_PREVIEW; + } + return SETTING_STATUS_NORMAL; + }; + + settings.sort((a, b) => { + const experimentalStatusA = getExperimentalStatus(a); + const experimentalStatusB = getExperimentalStatus(b); + if (experimentalStatusA !== experimentalStatusB) { + return experimentalStatusA - experimentalStatusB; + } + + const orderComparison = compareTwoNullableNumbers(a.order, b.order); + return orderComparison !== 0 ? orderComparison : a.key.localeCompare(b.key); + }); +} + function getMatchingSettings(allSettings: Set, filter: ITOCFilter): ISetting[] { const result: ISetting[] = []; @@ -637,31 +669,7 @@ function getMatchingSettings(allSettings: Set, filter: ITOCFilter): IS } }); - const SETTING_STATUS_NORMAL = 0; - const SETTING_STATUS_PREVIEW = 1; - const SETTING_STATUS_EXPERIMENTAL = 2; - - const getExperimentalStatus = (setting: ISetting) => { - if (setting.tags?.includes('experimental')) { - return SETTING_STATUS_EXPERIMENTAL; - } else if (setting.tags?.includes('preview')) { - return SETTING_STATUS_PREVIEW; - } - return SETTING_STATUS_NORMAL; - }; - - // Sort settings so that preview and experimental settings are deprioritized. - // Within each tier, sort the settings by order, then alphabetically. - return result.sort((a, b) => { - const experimentalStatusA = getExperimentalStatus(a); - const experimentalStatusB = getExperimentalStatus(b); - if (experimentalStatusA !== experimentalStatusB) { - return experimentalStatusA - experimentalStatusB; - } - - const orderComparison = compareTwoNullableNumbers(a.order, b.order); - return orderComparison !== 0 ? orderComparison : a.key.localeCompare(b.key); - }); + return result; } const settingPatternCache = new Map(); diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index bc8534ed4663f..be867aa3712b8 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -338,6 +338,7 @@ class OnAutoForwardedAction extends Disposable { private lastNotifyTime: Date; private static NOTIFY_COOL_DOWN = 5000; // milliseconds private lastNotification: INotificationHandle | undefined; + private readonly notificationDisposable = this._register(new MutableDisposable()); private lastShownPort: number | undefined; private doActionTunnels: RemoteTunnel[] | undefined; private alreadyOpenedOnce: Set = new Set(); @@ -479,7 +480,7 @@ class OnAutoForwardedAction extends Disposable { this.lastNotification = this.notificationService.prompt(Severity.Info, message, choices, { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } }); this.lastShownPort = tunnel.tunnelRemotePort; this.lastNotifyTime = new Date(); - this.lastNotification.onDidClose(() => { + this.notificationDisposable.value = this.lastNotification.onDidClose(() => { this.lastNotification = undefined; this.lastShownPort = undefined; }); @@ -540,7 +541,7 @@ class OnAutoForwardedAction extends Disposable { await this.basicMessage(newTunnel) + this.linkMessage(), [this.openBrowserChoice(newTunnel), this.openPreviewChoice(tunnel)], { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } }); - this.lastNotification.onDidClose(() => { + this.notificationDisposable.value = this.lastNotification.onDidClose(() => { this.lastNotification = undefined; this.lastShownPort = undefined; }); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index d1de3666a976f..c18c3d6ac8825 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -47,7 +47,7 @@ import { ITextFileService } from '../../../services/textfile/common/textfiles.js import { ITerminalGroupService, ITerminalService } from '../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../terminal/common/terminal.js'; -import { ConfiguringTask, ContributedTask, CustomTask, ExecutionEngine, InMemoryTask, InstancePolicy, ITaskConfig, ITaskEvent, ITaskIdentifier, ITaskInactiveEvent, ITaskProcessEndedEvent, ITaskSet, JsonSchemaVersion, KeyedTaskIdentifier, RerunAllRunningTasksCommandId, RuntimeType, Task, TASK_RUNNING_STATE, TaskDefinition, TaskEventKind, TaskGroup, TaskRunSource, TaskSettingId, TaskSorter, TaskSourceKind, TasksSchemaProperties, USER_TASKS_GROUP_KEY } from '../common/tasks.js'; +import { CommandString, ConfiguringTask, ContributedTask, CustomTask, ExecutionEngine, InMemoryTask, InstancePolicy, ITaskConfig, ITaskEvent, ITaskIdentifier, ITaskInactiveEvent, ITaskProcessEndedEvent, ITaskSet, JsonSchemaVersion, KeyedTaskIdentifier, RerunAllRunningTasksCommandId, RuntimeType, Task, TASK_RUNNING_STATE, TaskDefinition, TaskEventKind, TaskGroup, TaskRunSource, TaskSettingId, TaskSorter, TaskSourceKind, TasksSchemaProperties, USER_TASKS_GROUP_KEY } from '../common/tasks.js'; import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js'; import { CustomExecutionSupportedContext, ICustomizationProperties, IProblemMatcherRunOptions, ITaskFilter, ITaskProvider, ITaskService, IWorkspaceFolderTaskResult, ProcessExecutionSupportedContext, ServerlessWebContext, ShellExecutionSupportedContext, TaskCommandsRegistered, TaskExecutionSupportedContext, TasksAvailableContext } from '../common/taskService.js'; import { ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind, Triggers, VerifiedTask } from '../common/taskSystem.js'; @@ -1211,17 +1211,17 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.workspaceFolders.forEach(folder => { folderMap[folder.uri.toString()] = folder; }); - const folderToTasksMap: Map = new Map(); - const workspaceToTaskMap: Map = new Map(); + const folderToTasksMap: Map = new Map(); + const workspaceToTaskMap: Map = new Map(); const storedTasks = this._getTasksFromStorage(type); const tasks: (Task | ConfiguringTask)[] = []; this._log(nls.localize('taskService.getSavedTasks', 'Fetching tasks from task storage.'), true); - function addTaskToMap(map: Map, folder: string | undefined, task: any) { + function addTaskToMap(map: Map, folder: string | undefined, task: TaskConfig.ICustomTask | TaskConfig.IConfiguringTask) { if (folder && !map.has(folder)) { map.set(folder, []); } if (folder && (folderMap[folder] || (folder === USER_TASKS_GROUP_KEY)) && task) { - map.get(folder).push(task); + map.get(folder)!.push(task); } } for (const entry of storedTasks.entries()) { @@ -1238,7 +1238,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const readTasksMap: Map = new Map(); - async function readTasks(that: AbstractTaskService, map: Map, isWorkspaceFile: boolean) { + async function readTasks(that: AbstractTaskService, map: Map, isWorkspaceFile: boolean) { for (const key of map.keys()) { const custom: CustomTask[] = []; const customized: IStringDictionary = Object.create(null); @@ -1478,17 +1478,15 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (type === undefined) { return true; } - // eslint-disable-next-line local/code-no-any-casts - const settingValueMap: IStringDictionary = settingValue as any; + const settingValueMap: IStringDictionary = settingValue as IStringDictionary; return !settingValueMap[type]; } private _getTypeForTask(task: Task): string { let type: string; if (CustomTask.is(task)) { - const configProperties: TaskConfig.IConfigurationProperties = task._source.config.element; - // eslint-disable-next-line local/code-no-any-casts - type = (configProperties).type; + const configProperties = task._source.config.element as TaskConfig.ICustomTask; + type = configProperties.type ?? ''; } else { type = task.getDefinition()!.type; } @@ -1513,7 +1511,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return !task.hasDefinedMatchers && !!task.configurationProperties.problemMatchers && (task.configurationProperties.problemMatchers.length === 0); } if (CustomTask.is(task)) { - const configProperties: TaskConfig.IConfigurationProperties = task._source.config.element; + const configProperties = task._source.config.element as TaskConfig.IConfigurationProperties; return configProperties.problemMatcher === undefined && !task.hasDefinedMatchers; } return false; @@ -1526,8 +1524,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } let newValue: IStringDictionary; if (current !== false) { - // eslint-disable-next-line local/code-no-any-casts - newValue = current; + newValue = current as IStringDictionary; } else { newValue = Object.create(null); } @@ -1571,9 +1568,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer entries.unshift({ type: 'separator', label: nls.localize('TaskService.associate', 'associate') }); let taskType: string; if (CustomTask.is(task)) { - const configProperties: TaskConfig.IConfigurationProperties = task._source.config.element; - // eslint-disable-next-line local/code-no-any-casts - taskType = (configProperties).type; + const configProperties = task._source.config.element as TaskConfig.ICustomTask; + taskType = configProperties.type ?? ''; } else { taskType = task.getDefinition().type; } @@ -1726,8 +1722,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; const identifier: TaskConfig.ITaskIdentifier = Object.assign(Object.create(null), task.defines); delete identifier['_key']; - // eslint-disable-next-line local/code-no-any-casts - Object.keys(identifier).forEach(key => (toCustomize)![key] = identifier[key]); + Object.keys(identifier).forEach(key => (toCustomize as unknown as Record)![key] = identifier[key]); if (task.configurationProperties.problemMatchers && task.configurationProperties.problemMatchers.length > 0 && Types.isStringArray(task.configurationProperties.problemMatchers)) { toCustomize.problemMatcher = task.configurationProperties.problemMatchers; } @@ -1773,11 +1768,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const index: number | undefined = CustomTask.is(task) ? task._source.config.index : undefined; if (properties) { for (const property of Object.getOwnPropertyNames(properties)) { - // eslint-disable-next-line local/code-no-any-casts - const value = (properties)[property]; + const value = (properties as Record)[property]; if (value !== undefined && value !== null) { - // eslint-disable-next-line local/code-no-any-casts - (toCustomize)[property] = value; + (toCustomize as unknown as Record)[property] = value; } } } @@ -1791,7 +1784,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer '{', nls.localize('tasksJsonComment', '\t// See https://go.microsoft.com/fwlink/?LinkId=733558 \n\t// for the documentation about the tasks.json format'), ].join('\n') + JSON.stringify(value, null, '\t').substr(1); - const editorConfig = this._configurationService.getValue(); + const editorConfig = this._configurationService.getValue<{ editor: { insertSpaces: boolean; tabSize: number } }>(); if (editorConfig.editor.insertSpaces) { content = content.replace(/(\n)(\t+)/g, (_, s1, s2) => s1 + ' '.repeat(s2.length * editorConfig.editor.tabSize)); } @@ -1824,7 +1817,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private _writeConfiguration(workspaceFolder: IWorkspaceFolder, key: string, value: any, source?: string): Promise | undefined { + private _writeConfiguration(workspaceFolder: IWorkspaceFolder, key: string, value: unknown, source?: string): Promise | undefined { let target: ConfigurationTarget | undefined = undefined; switch (source) { case TaskSourceKind.User: target = ConfigurationTarget.USER; break; @@ -2321,12 +2314,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer resolve(result); } }; - const error = (error: any) => { + const error = (error: unknown) => { try { if (!isCancellationError(error)) { - if (error && Types.isString(error.message)) { - this._log(`Error: ${error.message}\n`); - this._showOutput(error.message); + if (error && Types.isString((error as { message?: string }).message)) { + this._log(`Error: ${(error as { message: string }).message}\n`); + this._showOutput(undefined, undefined, (error as { message: string }).message); } else { this._log('Unknown error received while collecting tasks from providers.'); this._showOutput(); @@ -2665,8 +2658,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!config) { return { config: undefined, hasParseErrors: false }; } - // eslint-disable-next-line local/code-no-any-casts - const parseErrors: string[] = (config as any).$parseErrors; + const parseErrors: string[] = (config as unknown as Record).$parseErrors as string[]; if (parseErrors) { let isAffected = false; for (const parseError of parseErrors) { @@ -2846,8 +2838,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!result) { return { config: undefined, hasParseErrors: false }; } - // eslint-disable-next-line local/code-no-any-casts - const parseErrors: string[] = (result as any).$parseErrors; + const parseErrors: string[] = (result as unknown as Record).$parseErrors as string[]; if (parseErrors) { let isAffected = false; for (const parseError of parseErrors) { @@ -2881,7 +2872,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; } - private _handleError(err: any): void { + private _handleError(err: unknown): void { let showOutput = true; if (err instanceof TaskError) { const buildError = err; @@ -2911,7 +2902,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._notificationService.error(nls.localize('TaskSystem.unknownError', 'An error has occurred while running a task. See task log for details.')); } if (showOutput) { - this._showOutput(undefined, undefined, err); + this._showOutput(undefined, undefined, Types.isString(err) ? err as string : undefined); } } @@ -3121,7 +3112,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return this._doRunTaskCommand(); } const type = typeof filter === 'string' ? undefined : filter.type; - const taskName = typeof filter === 'string' ? filter : filter.task; + const taskName = typeof filter === 'string' ? filter : filter.task as string; const grouped = await this._getGroupedTasks({ type }); const identifier = this._getTaskIdentifier(filter); const tasks = grouped.all(); @@ -3140,7 +3131,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } } - const exactMatchTask = !taskName ? undefined : tasks.find(t => t.configurationProperties.identifier === taskName || t.getDefinition(true)?.configurationProperties?.identifier === taskName); + const exactMatchTask = !taskName ? undefined : tasks.find(t => t.configurationProperties.identifier === taskName); if (!exactMatchTask) { return this._doRunTaskCommand(tasks, type, taskName); } @@ -3583,8 +3574,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return Promise.resolve(undefined); } content = pickTemplateResult.content; - // eslint-disable-next-line local/code-no-any-casts - const editorConfig = this._configurationService.getValue() as any; + const editorConfig = this._configurationService.getValue() as { editor: { insertSpaces: boolean; tabSize: number } }; if (editorConfig.editor.insertSpaces) { content = content.replace(/(\n)(\t+)/g, (_, s1, s2) => s1 + ' '.repeat(s2.length * editorConfig.editor.tabSize)); } @@ -3617,14 +3607,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } private _isTaskEntry(value: IQuickPickItem): value is IQuickPickItem & { task: Task } { - // eslint-disable-next-line local/code-no-any-casts - const candidate: IQuickPickItem & { task: Task } = value as any; + const candidate: IQuickPickItem & { task: Task } = value as IQuickPickItem & { task: Task }; return candidate && !!candidate.task; } private _isSettingEntry(value: IQuickPickItem): value is IQuickPickItem & { settingType: string } { - // eslint-disable-next-line local/code-no-any-casts - const candidate: IQuickPickItem & { settingType: string } = value as any; + const candidate: IQuickPickItem & { settingType: string } = value as IQuickPickItem & { settingType: string }; return candidate && !!candidate.settingType; } @@ -3734,8 +3722,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer })]); if (!timeout && ((await entries).length === 1) && this._configurationService.getValue(QUICKOPEN_SKIP_CONFIG)) { - const entry: any = (await entries)[0]; - if (entry.task) { + const entry = (await entries)[0] as TaskQuickPickEntryType; + if ((entry as IQuickPickItem & { task: Task }).task) { this._handleSelection(entry); return; } @@ -3752,8 +3740,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (cancellationToken.isCancellationRequested) { // canceled when there's only one task const task = (await entries)[0]; - // eslint-disable-next-line local/code-no-any-casts - if ((task).task) { + if ((task as IQuickPickItem & { task: Task }).task) { selection = task; } } @@ -3811,8 +3798,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (cancellationToken.isCancellationRequested) { // canceled when there's only one task const task = (await entries)[0]; - // eslint-disable-next-line local/code-no-any-casts - if ((task).task) { + if ((task as IQuickPickItem & { task: Task }).task) { entry = task; } } @@ -3970,13 +3956,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (task.command.name && !suppressTaskName && !globalConfig.windows?.command && !globalConfig.osx?.command && !globalConfig.linux?.command) { configElement.command = task.command.name; } else if (suppressTaskName) { - configElement.command = task._source.config.element.command; + configElement.command = (task._source.config.element as Record).command as string | CommandString; } if (task.command.args && (!Array.isArray(task.command.args) || (task.command.args.length > 0))) { if (!globalConfig.windows?.args && !globalConfig.osx?.args && !globalConfig.linux?.args) { configElement.args = task.command.args; } else { - configElement.args = task._source.config.element.args; + configElement.args = (task._source.config.element as Record).args as string[] | CommandString[]; } } } @@ -3988,7 +3974,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer configElement.isBackground = task.configurationProperties.isBackground; } if (task.configurationProperties.problemMatchers) { - configElement.problemMatcher = task._source.config.element.problemMatcher; + configElement.problemMatcher = (task._source.config.element as Record).problemMatcher as string[]; } if (task.configurationProperties.group) { configElement.group = task.configurationProperties.group; diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 080d0432909a4..2b029d727aa4e 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -144,7 +144,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut if (configuredTask._label) { taskNames.push(configuredTask._label); } else { - taskNames.push(configuredTask.configures.task); + taskNames.push(configuredTask.configures.task as string); } const location = this._getTaskSource(configuredTask._source); if (location) { @@ -227,7 +227,7 @@ export class ManageAutomaticTaskRunning extends Action2 { }); } - public async run(accessor: ServicesAccessor): Promise { + public async run(accessor: ServicesAccessor): Promise { const quickInputService = accessor.get(IQuickInputService); const configurationService = accessor.get(IConfigurationService); const allowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.allowAutomaticTasks', "Allow Automatic Tasks") }; diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 19a537dd101b3..4fa7237b82f4e 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -597,7 +597,7 @@ registerAction2(class extends Action2 { } }); } - async run(accessor: ServicesAccessor, args: any): Promise { + async run(accessor: ServicesAccessor, args: unknown): Promise { const terminalService = accessor.get(ITerminalService); const taskSystem = accessor.get(ITaskService); const instance = args as ITerminalInstance ?? terminalService.activeInstance; diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 20f2224f54a7f..4893f05b809f6 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1710,11 +1710,11 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this._collectMatcherVariables(variables, task.configurationProperties.problemMatchers); if (task.command.runtime === RuntimeType.CustomExecution && (CustomTask.is(task) || ContributedTask.is(task))) { - let definition: any; + let definition: Record | undefined; if (CustomTask.is(task)) { - definition = task._source.config.element; + definition = task._source.config.element as Record; } else { - definition = Objects.deepClone(task.defines); + definition = Objects.deepClone(task.defines) as Record; delete definition._key; delete definition.type; } @@ -1722,14 +1722,14 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } - private _collectDefinitionVariables(variables: Set, definition: any): void { + private _collectDefinitionVariables(variables: Set, definition: unknown): void { if (Types.isString(definition)) { this._collectVariables(variables, definition); } else if (Array.isArray(definition)) { - definition.forEach((element: any) => this._collectDefinitionVariables(variables, element)); + definition.forEach((element: unknown) => this._collectDefinitionVariables(variables, element)); } else if (Types.isObject(definition)) { for (const key of Object.keys(definition)) { - this._collectDefinitionVariables(variables, definition[key]); + this._collectDefinitionVariables(variables, (definition as Record)[key]); } } } @@ -1759,7 +1759,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const optionsEnv = options.env; if (optionsEnv) { Object.keys(optionsEnv).forEach((key) => { - const value: any = optionsEnv[key]; + const value = optionsEnv[key]; if (Types.isString(value)) { this._collectVariables(variables, value); } @@ -1912,11 +1912,11 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (options.env) { result.env = Object.create(null); for (const key of Object.keys(options.env)) { - const value: any = options.env[key]; + const value = options.env[key]; if (Types.isString(value)) { result.env![key] = await this._resolveVariable(resolver, value); } else { - result.env![key] = value.toString(); + result.env![key] = String(value); } } } diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts index 67017b41d2e9f..4b49951ab7d81 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts @@ -74,9 +74,13 @@ Object.getOwnPropertyNames(definitions).forEach(key => { delete definitions[key]; }); -function fixReferences(literal: any) { +function fixReferences(literal: Record | unknown[]) { if (Array.isArray(literal)) { - literal.forEach(fixReferences); + literal.forEach(element => { + if (typeof element === 'object' && element !== null) { + fixReferences(element as Record); + } + }); } else if (typeof literal === 'object') { if (literal['$ref']) { literal['$ref'] = literal['$ref'] + '1'; @@ -84,12 +88,12 @@ function fixReferences(literal: any) { Object.getOwnPropertyNames(literal).forEach(property => { const value = literal[property]; if (Array.isArray(value) || typeof value === 'object') { - fixReferences(value); + fixReferences(value as Record); } }); } } -fixReferences(schema); +fixReferences(schema as unknown as Record); ProblemMatcherRegistry.onReady().then(() => { try { diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index d917ab2e032f1..bdc6eb09448f0 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -15,9 +15,13 @@ import * as ConfigurationResolverUtils from '../../../services/configurationReso import { inputsSchema } from '../../../services/configurationResolver/common/configurationResolverSchema.js'; import { getAllCodicons } from '../../../../base/common/codicons.js'; -function fixReferences(literal: any) { +function fixReferences(literal: Record | unknown[]) { if (Array.isArray(literal)) { - literal.forEach(fixReferences); + literal.forEach(element => { + if (typeof element === 'object' && element !== null) { + fixReferences(element as Record); + } + }); } else if (typeof literal === 'object') { if (literal['$ref']) { literal['$ref'] = literal['$ref'] + '2'; @@ -25,7 +29,7 @@ function fixReferences(literal: any) { Object.getOwnPropertyNames(literal).forEach(property => { const value = literal[property]; if (Array.isArray(value) || typeof value === 'object') { - fixReferences(value); + fixReferences(value as Record); } }); } @@ -480,7 +484,7 @@ export function updateTaskDefinitions() { schemaProperties[key] = Objects.deepClone(property); } } - fixReferences(schema); + fixReferences(schema as unknown as Record); taskDefinitions.push(schema); } } @@ -647,7 +651,7 @@ Object.getOwnPropertyNames(definitions).forEach(key => { delete definitions[key]; deprecatedVariableMessage(definitions, newKey); }); -fixReferences(schema); +fixReferences(schema as unknown as Record); export function updateProblemMatchers() { try { diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index f769b8e174138..5d5fbbc473fc0 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -461,8 +461,8 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement } }); - this._register(markerChanged); // Ensures markerChanged is tracked and disposed of properly - + // Dispose the debounced listener after timeout - no need to register it since + // it's only used temporarily and will be disposed below setTimeout(() => { if (markerChanged) { const _markerChanged = markerChanged; diff --git a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts index a758a3e4e865c..fad04dd91bea1 100644 --- a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts @@ -358,8 +358,7 @@ abstract class AbstractLineMatcher implements ILineMatcher { if (trim) { value = Strings.trim(value)!; } - // eslint-disable-next-line local/code-no-any-casts - (data as any)[property] += endOfLine + value; + (data as Record)[property] = data[property]! + endOfLine + value; } } @@ -371,8 +370,7 @@ abstract class AbstractLineMatcher implements ILineMatcher { if (trim) { value = Strings.trim(value)!; } - // eslint-disable-next-line local/code-no-any-casts - (data as any)[property] = value; + (data as Record)[property] = value; } } } @@ -672,7 +670,7 @@ export namespace Config { } export namespace CheckedProblemPattern { - export function is(value: any): value is ICheckedProblemPattern { + export function is(value: unknown): value is ICheckedProblemPattern { const candidate: IProblemPattern = value as IProblemPattern; return candidate && Types.isString(candidate.regexp); } @@ -691,7 +689,7 @@ export namespace Config { } export namespace NamedProblemPattern { - export function is(value: any): value is INamedProblemPattern { + export function is(value: unknown): value is INamedProblemPattern { const candidate: INamedProblemPattern = value as INamedProblemPattern; return candidate && Types.isString(candidate.name); } @@ -706,7 +704,7 @@ export namespace Config { } export namespace NamedCheckedProblemPattern { - export function is(value: any): value is INamedCheckedProblemPattern { + export function is(value: unknown): value is INamedCheckedProblemPattern { const candidate: INamedProblemPattern = value as INamedProblemPattern; return candidate && NamedProblemPattern.is(candidate) && Types.isString(candidate.regexp); } @@ -715,15 +713,15 @@ export namespace Config { export type MultiLineProblemPattern = IProblemPattern[]; export namespace MultiLineProblemPattern { - export function is(value: any): value is MultiLineProblemPattern { - return value && Array.isArray(value); + export function is(value: unknown): value is MultiLineProblemPattern { + return Array.isArray(value); } } export type MultiLineCheckedProblemPattern = ICheckedProblemPattern[]; export namespace MultiLineCheckedProblemPattern { - export function is(value: any): value is MultiLineCheckedProblemPattern { + export function is(value: unknown): value is MultiLineCheckedProblemPattern { if (!MultiLineProblemPattern.is(value)) { return false; } @@ -754,7 +752,7 @@ export namespace Config { } export namespace NamedMultiLineCheckedProblemPattern { - export function is(value: any): value is INamedMultiLineCheckedProblemPattern { + export function is(value: unknown): value is INamedMultiLineCheckedProblemPattern { const candidate = value as INamedMultiLineCheckedProblemPattern; return candidate && Types.isString(candidate.name) && Array.isArray(candidate.patterns) && MultiLineCheckedProblemPattern.is(candidate.patterns); } @@ -937,7 +935,7 @@ export class ProblemPatternParser extends Parser { public parse(value: Config.MultiLineProblemPattern): MultiLineProblemPattern; public parse(value: Config.INamedProblemPattern): INamedProblemPattern; public parse(value: Config.INamedMultiLineCheckedProblemPattern): INamedMultiLineProblemPattern; - public parse(value: Config.IProblemPattern | Config.MultiLineProblemPattern | Config.INamedProblemPattern | Config.INamedMultiLineCheckedProblemPattern): any { + public parse(value: Config.IProblemPattern | Config.MultiLineProblemPattern | Config.INamedProblemPattern | Config.INamedMultiLineCheckedProblemPattern): IProblemPattern | MultiLineProblemPattern | INamedProblemPattern | INamedMultiLineProblemPattern | null { if (Config.NamedMultiLineCheckedProblemPattern.is(value)) { return this.createNamedMultiLineProblemPattern(value); } else if (Config.MultiLineCheckedProblemPattern.is(value)) { @@ -1015,8 +1013,7 @@ export class ProblemPatternParser extends Parser { function copyProperty(result: IProblemPattern, source: Config.IProblemPattern, resultKey: keyof IProblemPattern, sourceKey: keyof Config.IProblemPattern) { const value = source[sourceKey]; if (typeof value === 'number') { - // eslint-disable-next-line local/code-no-any-casts - (result as any)[resultKey] = value; + (result as unknown as Record)[resultKey] = value; } } copyProperty(result, value, 'file', 'file'); @@ -1906,8 +1903,7 @@ class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { } const matcher = this.get('tsc-watch'); if (matcher) { - // eslint-disable-next-line local/code-no-any-casts - (matcher).tscWatch = true; + (matcher as unknown as Record).tscWatch = true; } resolve(undefined); }); diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index e87fb5d13dc80..8751c6333804c 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -152,13 +152,13 @@ export interface IRunOptionsConfig { export interface ITaskIdentifier { type?: string; - [name: string]: any; + [name: string]: unknown; } export namespace ITaskIdentifier { - export function is(value: any): value is ITaskIdentifier { - const candidate: ITaskIdentifier = value; - return candidate !== undefined && Types.isString(value.type); + export function is(value: unknown): value is ITaskIdentifier { + const candidate: ITaskIdentifier = value as ITaskIdentifier; + return candidate !== undefined && Types.isString((value as ITaskIdentifier).type); } } @@ -556,7 +556,7 @@ type TaskConfigurationValueWithErrors = { errors?: string[]; }; -const EMPTY_ARRAY: any[] = []; +const EMPTY_ARRAY: never[] = []; Object.freeze(EMPTY_ARRAY); function assignProperty(target: T, source: Partial, key: K) { @@ -588,6 +588,7 @@ interface IMetaData { } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- IMetaData array holds heterogeneous parser types function _isEmpty(this: void, value: T | undefined, properties: IMetaData[] | undefined, allowEmptyArray: boolean = false): boolean { if (value === undefined || value === null || properties === undefined) { return true; @@ -605,6 +606,7 @@ function _isEmpty(this: void, value: T | undefined, properties: IMetaData(this: void, target: T | undefined, source: T | undefined, properties: IMetaData[]): T | undefined { if (!source || _isEmpty(source, properties)) { return target; @@ -614,19 +616,20 @@ function _assignProperties(this: void, target: T | undefined, source: T | und } for (const meta of properties) { const property = meta.property; - let value: any; + let value: T[keyof T] | undefined; if (meta.type !== undefined) { value = meta.type.assignProperties(target[property], source[property]); } else { value = source[property]; } if (value !== undefined && value !== null) { - target[property] = value; + (target as Record)[property as string] = value; } } return target; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- IMetaData array holds heterogeneous parser types function _fillProperties(this: void, target: T | undefined, source: T | undefined, properties: IMetaData[] | undefined, allowEmptyArray: boolean = false): T | undefined { if (!source || _isEmpty(source, properties)) { return target; @@ -636,19 +639,20 @@ function _fillProperties(this: void, target: T | undefined, source: T | undef } for (const meta of properties!) { const property = meta.property; - let value: any; + let value: T[keyof T] | undefined; if (meta.type) { value = meta.type.fillProperties(target[property], source[property]); } else if (target[property] === undefined) { value = source[property]; } if (value !== undefined && value !== null) { - target[property] = value; + (target as Record)[property as string] = value; } } return target; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- IMetaData array holds heterogeneous parser types function _fillDefaults(this: void, target: T | undefined, defaults: T | undefined, properties: IMetaData[], context: IParseContext): T | undefined { if (target && Object.isFrozen(target)) { return target; @@ -665,7 +669,7 @@ function _fillDefaults(this: void, target: T | undefined, defaults: T | undef if (target[property] !== undefined) { continue; } - let value: any; + let value: T[keyof T] | undefined; if (meta.type) { value = meta.type.fillDefaults(target[property], context); } else { @@ -673,12 +677,13 @@ function _fillDefaults(this: void, target: T | undefined, defaults: T | undef } if (value !== undefined && value !== null) { - target[property] = value; + (target as Record)[property as string] = value; } } return target; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- IMetaData array holds heterogeneous parser types function _freeze(this: void, target: T, properties: IMetaData[]): Readonly | undefined { if (target === undefined || target === null) { return undefined; @@ -772,8 +777,8 @@ namespace ShellConfiguration { const properties: IMetaData[] = [{ property: 'executable' }, { property: 'args' }, { property: 'quoting' }]; - export function is(value: any): value is IShellConfiguration { - const candidate: IShellConfiguration = value; + export function is(value: unknown): value is IShellConfiguration { + const candidate: IShellConfiguration = value as IShellConfiguration; return candidate && (Types.isString(candidate.executable) || Types.isStringArray(candidate.args)); } @@ -1005,6 +1010,7 @@ namespace CommandConfiguration { linux?: IBaseCommandConfigurationShape; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- IMetaData array holds heterogeneous parser types const properties: IMetaData[] = [ { property: 'runtime' }, { property: 'name' }, { property: 'options', type: CommandOptions }, { property: 'args' }, { property: 'taskSelector' }, { property: 'suppressTaskName' }, @@ -1196,14 +1202,15 @@ export namespace ProblemMatcherConverter { return result; } - export function fromWithOsConfig(this: void, external: IConfigurationProperties & { [key: string]: any }, context: IParseContext): TaskConfigurationValueWithErrors { + export function fromWithOsConfig(this: void, external: IConfigurationProperties & { [key: string]: unknown }, context: IParseContext): TaskConfigurationValueWithErrors { let result: TaskConfigurationValueWithErrors = {}; - if (external.windows && external.windows.problemMatcher && context.platform === Platform.Windows) { - result = from(external.windows.problemMatcher, context); - } else if (external.osx && external.osx.problemMatcher && context.platform === Platform.Mac) { - result = from(external.osx.problemMatcher, context); - } else if (external.linux && external.linux.problemMatcher && context.platform === Platform.Linux) { - result = from(external.linux.problemMatcher, context); + const osExternal = external as unknown as { windows?: { problemMatcher?: ProblemMatcherConfig.ProblemMatcherType }; osx?: { problemMatcher?: ProblemMatcherConfig.ProblemMatcherType }; linux?: { problemMatcher?: ProblemMatcherConfig.ProblemMatcherType } }; + if (osExternal.windows?.problemMatcher && context.platform === Platform.Windows) { + result = from(osExternal.windows.problemMatcher, context); + } else if (osExternal.osx?.problemMatcher && context.platform === Platform.Mac) { + result = from(osExternal.osx.problemMatcher, context); + } else if (osExternal.linux?.problemMatcher && context.platform === Platform.Linux) { + result = from(osExternal.linux.problemMatcher, context); } else if (external.problemMatcher) { result = from(external.problemMatcher, context); } @@ -1344,6 +1351,7 @@ namespace DependsOrder { namespace ConfigurationProperties { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- IMetaData array holds heterogeneous parser types const properties: IMetaData[] = [ { property: 'name' }, { property: 'identifier' }, @@ -1358,12 +1366,12 @@ namespace ConfigurationProperties { { property: 'hide' } ]; - export function from(this: void, external: IConfigurationProperties & { [key: string]: any }, context: IParseContext, + export function from(this: void, external: IConfigurationProperties & { [key: string]: unknown }, context: IParseContext, includeCommandOptions: boolean, source: TaskConfigSource, properties?: IJSONSchemaMap): TaskConfigurationValueWithErrors { if (!external) { return {}; } - const result: Tasks.IConfigurationProperties & { [key: string]: any } = {}; + const result: Tasks.IConfigurationProperties & { [key: string]: unknown } = {}; if (properties) { for (const propertyName of Object.keys(properties)) { @@ -1519,7 +1527,7 @@ namespace ConfiguringTask { RunOptions.fromConfiguration(external.runOptions), { hide: external.hide } ); - const configuration = ConfigurationProperties.from(external, context, true, source, typeDeclaration.properties); + const configuration = ConfigurationProperties.from(external as IConfigurationProperties & { [key: string]: unknown }, context, true, source, typeDeclaration.properties); result.addTaskLoadMessages(configuration.errors); if (configuration.value) { result.configurationProperties = Object.assign(result.configurationProperties, configuration.value); @@ -1597,7 +1605,7 @@ namespace CustomTask { identifier: taskName, } ); - const configuration = ConfigurationProperties.from(external, context, false, source); + const configuration = ConfigurationProperties.from(external as IConfigurationProperties & { [key: string]: unknown }, context, false, source); result.addTaskLoadMessages(configuration.errors); if (configuration.value) { result.configurationProperties = Object.assign(result.configurationProperties, configuration.value); @@ -1717,8 +1725,7 @@ export namespace TaskParser { function isCustomTask(value: ICustomTask | IConfiguringTask): value is ICustomTask { const type = value.type; - // eslint-disable-next-line local/code-no-any-casts - const customize = (value as any).customize; + const customize = (value as unknown as Record).customize; return customize === undefined && (type === undefined || type === null || type === Tasks.CUSTOMIZED_TASK_TYPE || type === 'shell' || type === 'process'); } diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index 5b5a12c84f528..ec05fe3438baa 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -126,7 +126,7 @@ export interface IResolvedVariables { export interface ITaskSystemInfo { platform: Platform; - context: any; + context: unknown; uriProvider: (this: void, path: string) => URI; resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: IResolveSet, target: ConfigurationTarget): Promise; findExecutable(command: string, cwd?: string, paths?: string[]): Promise; diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 623a81a275fba..e44d7c915f9e6 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -386,7 +386,7 @@ export namespace TaskGroup { export const Test: TaskGroup = { _id: 'test', isDefault: false }; - export function is(value: any): value is string { + export function is(value: unknown): value is string { return value === Clean._id || value === Build._id || value === Rebuild._id || value === Test._id; } @@ -436,7 +436,7 @@ export interface ITaskSourceConfigElement { workspace?: IWorkspace; file: string; index: number; - element: any; + element: unknown; } export interface ITaskConfig { @@ -471,7 +471,7 @@ export interface IExtensionTaskSource extends IBaseTaskSource { export interface IExtensionTaskSourceTransfer { __workspaceFolder: UriComponents; - __definition: { type: string;[name: string]: any }; + __definition: { type: string;[name: string]: unknown }; } export interface IInMemoryTaskSource extends IBaseTaskSource { @@ -494,7 +494,7 @@ export type TaskSource = IWorkspaceTaskSource | IExtensionTaskSource | IInMemory export type FileBasedTaskSource = IWorkspaceTaskSource | IUserTaskSource | WorkspaceFileTaskSource; export interface ITaskIdentifier { type: string; - [name: string]: any; + [name: string]: unknown; } export interface KeyedTaskIdentifier extends ITaskIdentifier { @@ -664,11 +664,10 @@ export abstract class CommonTask { } public clone(): Task { - // eslint-disable-next-line local/code-no-any-casts - return this.fromObject(Object.assign({}, this)); + return this.fromObject(Object.assign({}, this as unknown as Record)); } - protected abstract fromObject(object: any): Task; + protected abstract fromObject(object: Record): Task; public getWorkspaceFolder(): IWorkspaceFolder | undefined { return undefined; @@ -705,8 +704,7 @@ export abstract class CommonTask { public getTaskExecution(): ITaskExecution { const result: ITaskExecution = { id: this._id, - // eslint-disable-next-line local/code-no-any-casts - task: this + task: this as unknown as Task }; return result; } @@ -806,7 +804,7 @@ export class CustomTask extends CommonTask { } } - public static is(value: any): value is CustomTask { + public static is(value: unknown): value is CustomTask { return value instanceof CustomTask; } @@ -860,8 +858,9 @@ export class CustomTask extends CommonTask { } } - protected fromObject(object: CustomTask): CustomTask { - return new CustomTask(object._id, object._source, object._label, object.type, object.command, object.hasDefinedMatchers, object.runOptions, object.configurationProperties); + protected fromObject(object: Record): CustomTask { + const obj = object as unknown as CustomTask; + return new CustomTask(obj._id, obj._source, obj._label, obj.type, obj.command, obj.hasDefinedMatchers, obj.runOptions, obj.configurationProperties); } } @@ -886,12 +885,12 @@ export class ConfiguringTask extends CommonTask { this.configures = configures; } - public static is(value: any): value is ConfiguringTask { + public static is(value: unknown): value is ConfiguringTask { return value instanceof ConfiguringTask; } - protected fromObject(object: any): Task { - return object; + protected fromObject(object: Record): Task { + return object as unknown as Task; } public override getDefinition(): KeyedTaskIdentifier { @@ -980,7 +979,7 @@ export class ContributedTask extends CommonTask { return this.defines; } - public static is(value: any): value is ContributedTask { + public static is(value: unknown): value is ContributedTask { return value instanceof ContributedTask; } @@ -1019,8 +1018,9 @@ export class ContributedTask extends CommonTask { return 'extension'; } - protected fromObject(object: ContributedTask): ContributedTask { - return new ContributedTask(object._id, object._source, object._label, object.type, object.defines, object.command, object.hasDefinedMatchers, object.runOptions, object.configurationProperties); + protected fromObject(object: Record): ContributedTask { + const obj = object as unknown as ContributedTask; + return new ContributedTask(obj._id, obj._source, obj._label, obj.type, obj.defines, obj.command, obj.hasDefinedMatchers, obj.runOptions, obj.configurationProperties); } } @@ -1044,7 +1044,7 @@ export class InMemoryTask extends CommonTask { return new InMemoryTask(this._id, this._source, this._label, this.type, this.runOptions, this.configurationProperties); } - public static is(value: any): value is InMemoryTask { + public static is(value: unknown): value is InMemoryTask { return value instanceof InMemoryTask; } @@ -1060,8 +1060,9 @@ export class InMemoryTask extends CommonTask { return undefined; } - protected fromObject(object: InMemoryTask): InMemoryTask { - return new InMemoryTask(object._id, object._source, object._label, object.type, object.runOptions, object.configurationProperties); + protected fromObject(object: Record): InMemoryTask { + const obj = object as unknown as InMemoryTask; + return new InMemoryTask(obj._id, obj._source, obj._label, obj.type, obj.runOptions, obj.configurationProperties); } } @@ -1331,13 +1332,13 @@ export namespace TaskEvent { } export namespace KeyedTaskIdentifier { - function sortedStringify(literal: any): string { + function sortedStringify(literal: Record): string { const keys = Object.keys(literal).sort(); let result: string = ''; for (const key of keys) { let stringified = literal[key]; if (stringified instanceof Object) { - stringified = sortedStringify(stringified); + stringified = sortedStringify(stringified as Record); } else if (typeof stringified === 'string') { stringified = stringified.replace(/,/g, ',,'); } @@ -1347,7 +1348,7 @@ export namespace KeyedTaskIdentifier { } export function create(value: ITaskIdentifier): KeyedTaskIdentifier { const resultKey = sortedStringify(value); - const result = { _key: resultKey, type: value.taskType }; + const result = { _key: resultKey, type: value.taskType as string }; Object.assign(result, value); return result; } @@ -1390,7 +1391,7 @@ export namespace TaskDefinition { return KeyedTaskIdentifier.create(copy); } - const literal: { type: string;[name: string]: any } = Object.create(null); + const literal: { type: string;[name: string]: unknown } = Object.create(null); literal.type = definition.taskType; const required: Set = new Set(); definition.required.forEach(element => required.add(element)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 8b316cf49a546..3ec8c50c4fd7e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -474,12 +474,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } case TerminalCapability.CommandDetection: { e.capability.promptInputModel.setShellType(this.shellType); - capabilityListeners.set(e.id, Event.any( + // Use DisposableStore to track multiple listeners for this capability + const store = new DisposableStore(); + store.add(Event.any( e.capability.promptInputModel.onDidStartInput, e.capability.promptInputModel.onDidChangeInput, e.capability.promptInputModel.onDidFinishInput )(refreshInfo)); - this._register(e.capability.onCommandExecuted(async (command) => { + store.add(e.capability.onCommandExecuted(async (command) => { // Only generate ID if command doesn't already have one (i.e., it's a manual command, not Copilot-initiated) // The tool terminal sets the command ID before command start, so this won't override it if (!command.id && command.command) { @@ -488,6 +490,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { await this._processManager.setNextCommandId(command.command, commandId); } })); + capabilityListeners.set(e.id, store); break; } case TerminalCapability.PromptTypeDetection: { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index 8687162369364..8c70eb496af73 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -608,17 +608,38 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { ], {}, token); const suggestedOption = (await getTextResponseFromStream(response)).trim(); + const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts); + let validOption: string; + let index: number; + if (!suggestedOption) { - return; - } - const match = matchTerminalPromptOption(confirmationPrompt.options, suggestedOption); - if (!match.option || match.index === -1) { - return; + // No suggestion from LLM - fall back to first option if autoReply is enabled + if (autoReply) { + validOption = options[0]; + index = 0; + this._logService.trace(`OutputMonitor: No LLM suggestion, falling back to first option: ${validOption}`); + } else { + return; + } + } else { + const match = matchTerminalPromptOption(confirmationPrompt.options, suggestedOption); + if (!match.option || match.index === -1) { + // LLM suggestion didn't match any option - fall back to first option if autoReply is enabled + if (autoReply) { + validOption = options[0]; + index = 0; + this._logService.trace(`OutputMonitor: LLM suggestion '${suggestedOption}' didn't match options, falling back to first option: ${validOption}`); + } else { + return; + } + } else { + validOption = match.option; + index = match.index; + } } - const validOption = match.option; - const index = match.index; + let sentToTerminal = false; - if (this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts)) { + if (autoReply) { await this._execution.instance.sendText(validOption, true); this._outputMonitorTelemetryCounters.inputToolAutoAcceptCount++; this._outputMonitorTelemetryCounters.inputToolAutoChars += validOption?.length || 0; diff --git a/src/vs/workbench/contrib/terminalContrib/history/browser/terminal.history.contribution.ts b/src/vs/workbench/contrib/terminalContrib/history/browser/terminal.history.contribution.ts index 02f7760f0688c..55dfef250160c 100644 --- a/src/vs/workbench/contrib/terminalContrib/history/browser/terminal.history.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/history/browser/terminal.history.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { localize2 } from '../../../../../nls.js'; import { AccessibleViewProviderId } from '../../../../../platform/accessibility/browser/accessibleView.js'; @@ -46,24 +46,38 @@ class TerminalHistoryContribution extends Disposable implements ITerminalContrib this._terminalInRunCommandPicker = TerminalContextKeys.inTerminalRunCommandPicker.bindTo(contextKeyService); + // Track listeners per capability to avoid leaking disposables + const capabilityListeners = this._register(new DisposableMap()); + this._register(_ctx.instance.capabilities.onDidAddCapability(e => { + // Dispose any existing listener for this capability before adding new one + capabilityListeners.deleteAndDispose(e.id); + switch (e.id) { case TerminalCapability.CwdDetection: { - this._register(e.capability.onDidChangeCwd(e => { + const store = new DisposableStore(); + store.add(e.capability.onDidChangeCwd(e => { this._instantiationService.invokeFunction(getDirectoryHistory)?.add(e, { remoteAuthority: _ctx.instance.remoteAuthority }); })); + capabilityListeners.set(e.id, store); break; } case TerminalCapability.CommandDetection: { - this._register(e.capability.onCommandFinished(e => { + const store = new DisposableStore(); + store.add(e.capability.onCommandFinished(e => { if (e.command.trim().length > 0) { this._instantiationService.invokeFunction(getCommandHistory)?.add(e.command, { shellType: _ctx.instance.shellType }); } })); + capabilityListeners.set(e.id, store); break; } } })); + + this._register(_ctx.instance.capabilities.onDidRemoveCapability(e => { + capabilityListeners.deleteAndDispose(e.id); + })); } /** diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 94603fb67e687..7d207e48c414d 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -36,6 +36,8 @@ import { Schemas } from '../../../../base/common/network.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { dirname } from '../../../../base/common/resources.js'; import { asWebviewUri } from '../../webview/common/webview.js'; +import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; export class ReleaseNotesManager extends Disposable { private readonly _simpleSettingRenderer: SimpleSettingRenderer; @@ -58,6 +60,8 @@ export class ReleaseNotesManager extends Disposable { @IExtensionService private readonly _extensionService: IExtensionService, @IProductService private readonly _productService: IProductService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IUpdateService private readonly _updateService: IUpdateService, + @ICommandService private readonly _commandService: ICommandService, ) { super(); @@ -67,6 +71,7 @@ export class ReleaseNotesManager extends Disposable { this._register(_configurationService.onDidChangeConfiguration((e) => this.onDidChangeConfiguration(e))); this._register(_webviewWorkbenchService.onDidChangeActiveWebviewEditor((e) => this.onDidChangeActiveWebviewEditor(e))); + this._register(this._updateService.onStateChange(() => this.postUpdateAction())); this._simpleSettingRenderer = this._instantiationService.createInstance(SimpleSettingRenderer); } @@ -129,6 +134,10 @@ export class ReleaseNotesManager extends Disposable { disposables.add(this._currentReleaseNotes.webview.onMessage(e => { if (e.message.type === 'showReleaseNotes') { this._configurationService.updateValue('update.showReleaseNotes', e.message.value); + } else if (e.message.type === 'updateAction') { + if (e.message.commandId) { + this._commandService.executeCommand(e.message.commandId); + } } else if (e.message.type === 'clickSetting') { const x = this._currentReleaseNotes?.webview.container.offsetLeft + e.message.value.x; const y = this._currentReleaseNotes?.webview.container.offsetTop + e.message.value.y; @@ -271,6 +280,7 @@ export class ReleaseNotesManager extends Disposable { const colorMap = TokenizationRegistry.getColorMap(); const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; const showReleaseNotes = Boolean(this._configurationService.getValue('update.showReleaseNotes')); + const updateAction = this.getUpdateAction(); return ` @@ -531,6 +541,80 @@ export class ReleaseNotesManager extends Disposable { margin-left: 0; } } + + /* Update action button */ + #update-action-btn { + position: fixed; + right: 25px; + top: 25px; + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: 1px solid var(--vscode-button-border, transparent); + border-radius: 50%; + width: 40px; + height: 40px; + padding: 0; + cursor: pointer; + font-size: var(--vscode-font-size); + font-family: var(--vscode-font-family); + white-space: nowrap; + box-shadow: 1px 1px 1px rgba(0,0,0,.25); + z-index: 100; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + transition: border-radius 0.25s ease, padding 0.25s ease, width 0.25s ease; + } + + #update-action-btn .icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + } + + #update-action-btn .icon svg { + width: 16px; + height: 16px; + display: block; + } + + #update-action-btn .label { + overflow: hidden; + max-width: 0; + opacity: 0; + margin-left: 0; + transition: max-width 0.25s ease, opacity 0.2s ease, margin-left 0.25s ease; + } + + #update-action-btn:hover, + #update-action-btn.expanded { + background-color: var(--vscode-button-hoverBackground); + box-shadow: 2px 2px 2px rgba(0,0,0,.25); + width: auto; + border-radius: 20px; + padding: 0 14px; + } + + #update-action-btn:hover .label, + #update-action-btn.expanded .label { + max-width: 200px; + opacity: 1; + margin-left: 6px; + } + + #update-action-btn.expanded { + background-color: var(--vscode-button-background); + } + + body.vscode-high-contrast #update-action-btn { + border-width: 2px; + border-style: solid; + box-shadow: none; + } @@ -562,6 +646,15 @@ export class ReleaseNotesManager extends Disposable { window.addEventListener('message', event => { if (event.data.type === 'showReleaseNotes') { input.checked = event.data.value; + } else if (event.data.type === 'updateAction') { + if (event.data.label) { + updateActionBtn.querySelector('.label').textContent = event.data.label; + updateActionBtn.dataset.commandId = event.data.commandId; + updateActionBtn.setAttribute('aria-label', event.data.label); + updateActionBtn.style.display = 'flex'; + } else { + updateActionBtn.style.display = 'none'; + } } }); @@ -584,11 +677,79 @@ export class ReleaseNotesManager extends Disposable { input.addEventListener('change', event => { vscode.postMessage({ type: 'showReleaseNotes', value: input.checked }, '*'); }); + + // Update action button + const updateActionBtn = document.createElement('button'); + updateActionBtn.id = 'update-action-btn'; + + // Arrow-circle-down SVG icon + const iconSpan = document.createElement('span'); + iconSpan.className = 'icon'; + iconSpan.innerHTML = ''; + updateActionBtn.appendChild(iconSpan); + + const labelSpan = document.createElement('span'); + labelSpan.className = 'label'; + updateActionBtn.appendChild(labelSpan); + + const initialAction = ${JSON.stringify(updateAction ?? null)}; + if (initialAction) { + labelSpan.textContent = initialAction.label; + updateActionBtn.dataset.commandId = initialAction.commandId; + updateActionBtn.setAttribute('aria-label', initialAction.label); + updateActionBtn.style.display = 'flex'; + } else { + updateActionBtn.style.display = 'none'; + } + + document.body.appendChild(updateActionBtn); + + updateActionBtn.addEventListener('click', () => { + if (updateActionBtn.dataset.commandId) { + vscode.postMessage({ type: 'updateAction', commandId: updateActionBtn.dataset.commandId }); + } + }); + + // Expand button when at top of page + function updateExpandedState() { + if (window.scrollY <= 100) { + updateActionBtn.classList.add('expanded'); + } else { + updateActionBtn.classList.remove('expanded'); + } + } + updateExpandedState(); + window.addEventListener('scroll', updateExpandedState); `; } + private getUpdateAction(): { label: string; commandId: string } | undefined { + const state = this._updateService.state; + switch (state.type) { + case StateType.AvailableForDownload: + return { label: nls.localize('releaseNotes.downloadUpdate', "Download Update"), commandId: 'update.downloadNow' }; + case StateType.Downloaded: + return { label: nls.localize('releaseNotes.installUpdate', "Install Update"), commandId: 'update.install' }; + case StateType.Ready: + return { label: nls.localize('releaseNotes.restartToUpdate', "Restart to Update"), commandId: 'update.restart' }; + default: + return undefined; + } + } + + private postUpdateAction(): void { + if (this._currentReleaseNotes) { + const action = this.getUpdateAction(); + this._currentReleaseNotes.webview.postMessage({ + type: 'updateAction', + label: action?.label ?? '', + commandId: action?.commandId ?? '' + }); + } + } + private onDidChangeConfiguration(e: IConfigurationChangeEvent): void { if (e.affectsConfiguration('update.showReleaseNotes')) { this.updateCheckboxWebview(); diff --git a/src/vs/workbench/services/chat/common/chatEntitlementService.ts b/src/vs/workbench/services/chat/common/chatEntitlementService.ts index 7c5333c0d5327..adca06c9c5aa8 100644 --- a/src/vs/workbench/services/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/services/chat/common/chatEntitlementService.ts @@ -140,6 +140,8 @@ export interface IChatEntitlementService { readonly entitlement: ChatEntitlement; readonly entitlementObs: IObservable; + readonly previewFeaturesDisabled: boolean; + readonly organisations: string[] | undefined; readonly isInternal: boolean; readonly sku: string | undefined; @@ -373,6 +375,10 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme return this.context?.value.state.copilotTrackingId; } + get previewFeaturesDisabled(): boolean { + return this.contextKeyService.getContextKeyValue('github.copilot.previewFeaturesDisabled') === true; + } + //#endregion //#region --- Quotas diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 1210587c97ece..65b1d6b58ce8b 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -26,7 +26,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IRemoteAgentEnvironment } from '../../../../platform/remote/common/remoteAgentEnvironment.js'; import { isValidBasename } from '../../../../base/common/extpath.js'; import { Emitter } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { createCancelablePromise, CancelablePromise } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ICommandHandler } from '../../../../platform/commands/common/commands.js'; @@ -378,10 +378,11 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { } })); + const busyDisposable = this._register(new MutableDisposable()); const handleAccept = () => { if (this.busy) { // Save the accept until the file picker is not busy. - this.onBusyChangeEmitter.event((busy: boolean) => { + busyDisposable.value = this.onBusyChangeEmitter.event((busy: boolean) => { if (!busy) { handleAccept(); } diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index e7f4c5c2262c3..c1c1f94f062bd 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -214,7 +214,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } async openPreferences(): Promise { - await this.editorService.openEditor(this.instantiationService.createInstance(PreferencesEditorInput)); + await this.editorService.openEditor(this.instantiationService.createInstance(PreferencesEditorInput), undefined, MODAL_GROUP); } openSettings(options: IOpenSettingsOptions = {}): Promise { @@ -273,7 +273,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic ...options, focusSearch: true }; - const group = this.getEditorGroupFromOptions(options); + const group = this.getEditorGroupFromOptions(false, options); return this.editorService.openEditor(input, validateSettingsEditorOptions(options), group); } @@ -358,7 +358,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } } else { - const group = this.getEditorGroupFromOptions(options); + const group = this.getEditorGroupFromOptions(false, options); const editor = (await this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { ...options }, group)) as IKeybindingsEditorPane; if (options.query) { editor.search(options.query); @@ -371,13 +371,13 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorService.openEditor({ resource: this.defaultKeybindingsResource, label: nls.localize('defaultKeybindings', "Default Keybindings") }); } - private getEditorGroupFromOptions(options: { groupId?: number; openInModal?: boolean; openToSide?: boolean }): PreferredGroup { + private getEditorGroupFromOptions(isTextual: boolean, options: { groupId?: number; openToSide?: boolean }): PreferredGroup { + if (!isTextual && this.configurationService.getValue('workbench.editor.allowOpenInModalEditor')) { + return MODAL_GROUP; + } if (options.openToSide) { return SIDE_GROUP; } - if (options.openInModal) { - return MODAL_GROUP; - } if (options?.groupId !== undefined) { return this.editorGroupService.getGroup(options.groupId) ?? this.editorGroupService.activeGroup; } @@ -385,7 +385,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } private async openSettingsJson(resource: URI, options: IOpenSettingsOptions): Promise { - const group = this.getEditorGroupFromOptions(options); + const group = this.getEditorGroupFromOptions(true, options); const editor = await this.doOpenSettingsJson(resource, options, group); if (editor && options?.revealSetting) { await this.revealSetting(options.revealSetting.key, !!options.revealSetting.edit, editor, resource); diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index c6ffca6aad9f6..4fc67f487988b 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -222,7 +222,6 @@ export interface ISettingsEditorOptions extends IEditorOptions { export interface IOpenSettingsOptions extends ISettingsEditorOptions { jsonEditor?: boolean; openToSide?: boolean; - openInModal?: boolean; groupId?: number; } @@ -246,7 +245,6 @@ export interface IKeybindingsEditorOptions extends IEditorOptions { export interface IOpenKeybindingsEditorOptions extends IKeybindingsEditorOptions { groupId?: number; - openInModal?: boolean; } export const IPreferencesService = createDecorator('preferencesService'); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 271301ca45285..a2b918d627a2d 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -57,6 +57,7 @@ import { IFolderBackupInfo, IWorkspaceBackupInfo } from '../../../platform/backu import { ConfigurationTarget, IConfigurationService, IConfigurationValue } from '../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js'; import { ContextKeyValue, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; +import { IDefaultAccountService } from '../../../platform/defaultAccount/common/defaultAccount.js'; import { ContextMenuService } from '../../../platform/contextview/browser/contextMenuService.js'; import { IContextMenuMenuDelegate, IContextMenuService, IContextViewService } from '../../../platform/contextview/browser/contextView.js'; import { ContextViewService } from '../../../platform/contextview/browser/contextViewService.js'; @@ -189,6 +190,7 @@ import { IWorkingCopyEditorService, WorkingCopyEditorService } from '../../servi import { IWorkingCopyFileService, WorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js'; import { IWorkingCopyService, WorkingCopyService } from '../../services/workingCopy/common/workingCopyService.js'; import { TestChatEntitlementService, TestContextService, TestExtensionService, TestFileService, TestHistoryService, TestLifecycleService, TestLoggerService, TestMarkerService, TestProductService, TestStorageService, TestTextResourcePropertiesService, TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from '../common/workbenchTestServices.js'; +import { DefaultAccountService } from '../../services/accounts/browser/defaultAccount.js'; // Backcompat export export { TestFileService, TestLifecycleService }; @@ -378,6 +380,7 @@ export function workbenchInstantiationService( instantiationService.stub(IChatEntitlementService, new TestChatEntitlementService()); instantiationService.stub(IMarkdownRendererService, instantiationService.createInstance(MarkdownRendererService)); instantiationService.stub(IChatWidgetService, instantiationService.createInstance(TestChatWidgetService)); + instantiationService.stub(IDefaultAccountService, DefaultAccountService); return instantiationService; } diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index fee1ed7c1fb7e..25ebf2bcc5281 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -805,6 +805,8 @@ export class TestChatEntitlementService implements IChatEntitlementService { readonly anonymous = false; onDidChangeAnonymous = Event.None; readonly anonymousObs = observableValue({}, false); + + readonly previewFeaturesDisabled = false; } export class TestLifecycleService extends Disposable implements ILifecycleService { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 928085d2f93fc..7239fa186edea 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -116,6 +116,7 @@ import './services/authentication/browser/authenticationMcpService.js'; import './services/authentication/browser/dynamicAuthenticationProviderStorageService.js'; import './services/authentication/browser/authenticationQueryService.js'; import '../platform/hover/browser/hoverService.js'; +import '../platform/userInteraction/browser/userInteractionServiceImpl.js'; import './services/assignment/common/assignmentService.js'; import './services/outline/browser/outlineService.js'; import './services/languageDetection/browser/languageDetectionWorkerServiceImpl.js'; diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 49e04398f6d59..214a366c69c54 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "1.25.2", + "@modelcontextprotocol/sdk": "1.26.0", "minimist": "^1.2.8", "ncp": "^2.0.0", "node-fetch": "^2.6.7" @@ -22,9 +22,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", - "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -34,12 +34,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", - "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -47,14 +47,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -488,10 +489,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, "engines": { "node": ">= 16" }, @@ -702,7 +706,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -749,6 +752,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1470,9 +1482,9 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", - "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" diff --git a/test/mcp/package.json b/test/mcp/package.json index f4829b826d458..917be8576b8c1 100644 --- a/test/mcp/package.json +++ b/test/mcp/package.json @@ -12,7 +12,7 @@ "start-stdio": "echo 'Starting vscode-automation-mcp... For customization and troubleshooting, see ./test/mcp/README.md' && npm ci && npm run -s compile && node ./out/stdio.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "1.25.2", + "@modelcontextprotocol/sdk": "1.26.0", "minimist": "^1.2.8", "ncp": "^2.0.0", "node-fetch": "^2.6.7"