diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index f8dcaac522a5f..a46bef0bbab71 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1041,7 +1041,7 @@ "--animation-angle", "--animation-opacity", "--chat-input-anim-angle", - "--chat-input-working-fill", + "--chat-send-button-anim-angle", "--chat-setup-dialog-glow-angle", "--vscode-chat-font-family", "--vscode-chat-font-size-body-l", diff --git a/extensions/copilot/docs/NES_EXPECTED_EDIT_CAPTURE.md b/extensions/copilot/docs/NES_EXPECTED_EDIT_CAPTURE.md index 9f69e9886da4f..217c6f3932eac 100644 --- a/extensions/copilot/docs/NES_EXPECTED_EDIT_CAPTURE.md +++ b/extensions/copilot/docs/NES_EXPECTED_EDIT_CAPTURE.md @@ -16,7 +16,7 @@ Add this setting to your VS Code `settings.json`: } ``` -That's it! Auto-capture on rejection is enabled by default. To disable it (you can still capture manually via **Cmd+K Cmd+R**): +That's it! Auto-capture on rejection is enabled by default. To disable it (you can still capture manually via the Command Palette): ```json { "github.copilot.chat.advanced.inlineEdits.recordExpectedEdit.onReject": false @@ -29,14 +29,14 @@ That's it! Auto-capture on rejection is enabled by default. To disable it (you c 1. Reject the suggestion (press `Esc` or continue typing) 2. If `onReject` is enabled, capture mode starts automatically 3. Type the code you *expected* NES to suggest -4. Press **Enter** to save, or **Esc** to cancel +4. Press **Cmd+Enter** (Mac) / **Ctrl+Enter** (Windows/Linux) to save, or **Esc** to cancel **When NES didn't appear but should have:** -1. Press **Cmd+K Cmd+R** (Mac) or **Ctrl+K Ctrl+R** (Windows/Linux) +1. Open the Command Palette (**Cmd+Shift+P** / **Ctrl+Shift+P**) and run **"Copilot: Record Expected Edit (NES)"** 2. Type the code you expected NES to suggest -3. Press **Enter** to save +3. Press **Cmd+Enter** / **Ctrl+Enter** to save -> **Tip:** Use **Shift+Enter** to insert newlines during capture (since Enter saves). +> **Tip:** To indicate that *no* edit was expected (i.e. the rejection was correct), press **Cmd+Enter** / **Ctrl+Enter** without making any edits. ### 3. Submit Your Feedback Once you've captured some edits: @@ -49,10 +49,10 @@ Once you've captured some edits: | Action | Keybinding | |--------|------------| -| Start capture manually | **Cmd+K Cmd+R** / **Ctrl+K Ctrl+R** | -| Save capture | **Enter** | +| Start capture manually | Command Palette → **Copilot: Record Expected Edit (NES)** | +| Save capture | **Cmd+Enter** / **Ctrl+Enter** | +| Save as "no edit expected" | **Cmd+Enter** / **Ctrl+Enter** with no edits made | | Cancel capture | **Esc** | -| Insert newline | **Shift+Enter** | | Command | Description | |---------|-------------| @@ -63,13 +63,13 @@ Once you've captured some edits: ### Trigger Points - **Automatic**: Capture starts when you reject an NES suggestion (if `onReject` setting is enabled) -- **Manual**: Use the keyboard shortcut or Command Palette when NES didn't appear but should have +- **Manual**: Run **"Copilot: Record Expected Edit (NES)"** from the Command Palette when NES didn't appear but should have ### Capture Session When capture mode is active: -1. A status bar indicator shows: **"NES CAPTURE MODE ACTIVE"** -2. Type your expected edit naturally in the editor -3. Press **Enter** to save or **Esc** to cancel +1. An animated status bar indicator shows: **"NES CAPTURE MODE ACTIVE"** (with an error background for visibility). Hovering it reveals the available keybindings. +2. Type your expected edit naturally in the editor (Enter inserts a newline as usual) +3. Press **Cmd+Enter** / **Ctrl+Enter** to save (saving with no edits records "no edit expected"), or **Esc** to cancel ### Where Captures Are Saved Recordings are stored in your workspace under `.copilot/nes-feedback/`: diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 394b8dbcd09c1..e7b122547ef81 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -13,7 +13,7 @@ "@anthropic-ai/claude-agent-sdk": "0.2.112", "@anthropic-ai/sdk": "^0.82.0", "@github/blackbird-external-ingest-utils": "^0.3.0", - "@github/copilot": "^1.0.28", + "@github/copilot": "^1.0.34", "@google/genai": "^1.22.0", "@humanwhocodes/gitignore-to-minimatch": "1.0.2", "@microsoft/tiktokenizer": "^1.0.10", @@ -3203,26 +3203,26 @@ "license": "MIT" }, "node_modules/@github/copilot": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.28.tgz", - "integrity": "sha512-S1Y+KnhywjIsK1DzskoCqPVC3uURohvCRyDkGPWXvMw+lXO5ryOJvHFZDDw7MSRjT7ea7T0m8e3yKdK0OxJhnw==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.34.tgz", + "integrity": "sha512-jFYulj1v00b3j43Er9+WwhZ/XldGq7+gti2s2pRhrdPwYEd1PMvscDZwRa/1iUBz/XQ5HUGac1tD8P7+VUpWjg==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.28", - "@github/copilot-darwin-x64": "1.0.28", - "@github/copilot-linux-arm64": "1.0.28", - "@github/copilot-linux-x64": "1.0.28", - "@github/copilot-win32-arm64": "1.0.28", - "@github/copilot-win32-x64": "1.0.28" + "@github/copilot-darwin-arm64": "1.0.34", + "@github/copilot-darwin-x64": "1.0.34", + "@github/copilot-linux-arm64": "1.0.34", + "@github/copilot-linux-x64": "1.0.34", + "@github/copilot-win32-arm64": "1.0.34", + "@github/copilot-win32-x64": "1.0.34" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.28.tgz", - "integrity": "sha512-Bkis5dkOsdgaK95j/8mgIGSxHlRuL211Wa3S4MeeYGrilZweaG20sa0jktzagL6XFxfPRKBC87E+fDFyXz1L3g==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.34.tgz", + "integrity": "sha512-g94EhSLd3a6fckZ6xb/zP2DZJZEx7kONWdOoDiHXUtSqc4RiZ7OBq1EwT4WrPY1lsmy9sioJIcZSGzJd0C1M7Q==", "cpu": [ "arm64" ], @@ -3236,9 +3236,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.28.tgz", - "integrity": "sha512-0RIabmr05KgPPUcD4kpKNBGg/eRwJF2NrYtibDUCIRFWKZu7q0m9c9EURpW0wOO32cXZtAQ+BmJIGlqfCkt6gA==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.34.tgz", + "integrity": "sha512-tIgFEZV0ohCF/VgTODJWre3xURsvEd+6IPN/HPKWxG6AXtJOxzjlr5kLYYdPHdNlHNmSxGQw8fWsN2FZ4nyDdw==", "cpu": [ "x64" ], @@ -3252,9 +3252,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.28.tgz", - "integrity": "sha512-A/zQ4ifN+FSSEHdPHajv5UwygS5BOQ8l1AJMYdVBnnuqVX9bCcRAJJ4S/F60AnaDimzDvVuYSe3lYXRYxz3M5A==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.34.tgz", + "integrity": "sha512-feqjEetrlqBUhYskIsPmwACQOWO99cvRpKwIFl3OlEjWoj+//HA7yXh49UIe0gD8wQUI8hy05uVz3K2/xti2nQ==", "cpu": [ "arm64" ], @@ -3268,9 +3268,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.28.tgz", - "integrity": "sha512-0VqoW9hj7qKj+eH2un9E7zn9AbassTZHkKQPsd8yPvLsmPaNJgsHMYDrCCNZNol2ZSGt/XskTfmWQaQM6BoBfg==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.34.tgz", + "integrity": "sha512-3l0rZZqmceklHizJaaO+Iy2PsAZpVZS9Mn9VYnVcY/8Yzt4Y2hmXSFcKVfc4l+JlhFsPs7trhMdIkfwkjaKPLg==", "cpu": [ "x64" ], @@ -3284,9 +3284,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.28.tgz", - "integrity": "sha512-f28NKudBtIXTpIliHGJbRhEfCItsXKWNzXzgqgmP8FZB+JYrqG/ysU2qCUCxhpv3PLjMLWqnsWs+mIvVLTH9zw==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.34.tgz", + "integrity": "sha512-06kEJO3iyohmAqF4iIbOxOfWLFSIpLDJ1L1oEHRtouMrH2Ll1wrUjsoQT1gXgBOv7rifl25qx/Avx5zKqvuORw==", "cpu": [ "arm64" ], @@ -3300,9 +3300,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.28.tgz", - "integrity": "sha512-b9ZEx2i5P7DZTP66FXTfwf81r5kbAqs2GEJjDdevCwxH7cRexqM9eBxQGj1zGtm4qXF7JGK2eH6Ay7NC28m1Iw==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.34.tgz", + "integrity": "sha512-QLL8pS4q2TTyQbClEXxqXtQGPr4lk+pwc8hPMUL7iw7HGDOvs1WCLMT1ZSDPPcxSrTnR/dURX5za1NMA8uF/fw==", "cpu": [ "x64" ], diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 9028db141f0ff..8525cd4b5cb56 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -4222,6 +4222,33 @@ "onExp" ] }, + "github.copilot.chat.inlineChat.reasoningEffort": { + "type": "string", + "default": "low", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high" + ], + "markdownDescription": "%github.copilot.config.inlineChat.reasoningEffort%", + "tags": [ + "advanced", + "experimental", + "onExp" + ] + }, + "github.copilot.chat.inlineChat.enableThinking": { + "type": "boolean", + "default": false, + "markdownDescription": "%github.copilot.config.inlineChat.enableThinking%", + "tags": [ + "advanced", + "experimental", + "onExp" + ] + }, "github.copilot.chat.debug.requestLogger.maxEntries": { "type": "number", "default": 100, @@ -5682,7 +5709,7 @@ { "command": "github.copilot.sessions.discardChanges", "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && sessions.changesVersionMode == branchChanges", - "group": "navigation@1" + "group": "navigation@2" } ], "chat/customizations/create": [ @@ -6411,7 +6438,7 @@ "@anthropic-ai/claude-agent-sdk": "0.2.112", "@anthropic-ai/sdk": "^0.82.0", "@github/blackbird-external-ingest-utils": "^0.3.0", - "@github/copilot": "^1.0.28", + "@github/copilot": "^1.0.34", "@google/genai": "^1.22.0", "@humanwhocodes/gitignore-to-minimatch": "1.0.2", "@microsoft/tiktokenizer": "^1.0.10", diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index 0b4674414975e..7d7f9d690d313 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -374,6 +374,8 @@ "github.copilot.config.localWorkspaceRecording.enabled": "Enable local workspace recording for analysis.", "github.copilot.config.editRecording.enabled": "Enable edit recording for analysis.", "github.copilot.config.inlineChat.selectionRatioThreshold": "Threshold at which to switch editing strategies for inline chat. When a selection portion of code matches a parse tree node, only that is presented to the language model. This speeds up response times but might have lower quality results. Requires having a parse tree for the document and the `inlineChat.enableV2` setting. Values must be between 0 and 1, where 0 means off and 1 means the selection perfectly matches a parse tree node.", + "github.copilot.config.inlineChat.reasoningEffort": "Controls the reasoning effort level for inline chat requests. Lower values result in faster responses with fewer reasoning tokens. Supported values depend on the model.", + "github.copilot.config.inlineChat.enableThinking": "Controls whether thinking/reasoning is enabled for inline chat requests. When disabled, reasoning summaries are suppressed for faster responses.", "github.copilot.config.debug.requestLogger.maxEntries": "Maximum number of entries to keep in the request logger for debugging purposes.", "github.copilot.config.chat.agentDebugLog.enabled": "Deprecated: use `github.copilot.chat.agentDebugLog.fileLogging.enabled` instead.", "github.copilot.config.chat.agentDebugLog.enabled.deprecated": "This setting has been merged into `github.copilot.chat.agentDebugLog.fileLogging.enabled`. Please use this setting instead.", diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts index 5b323ae81378c..3d670a8797f96 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts @@ -178,7 +178,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS this.monitorSessionFiles(); this._sessionManager = new Lazy>(async () => { try { - const { internal, createLocalFeatureFlagService } = await this.getSDKPackage(); + const { internal, createLocalFeatureFlagService, AutoModeSessionManager } = await this.getSDKPackage(); // Always enable SDK OTel so the debug panel receives native spans via the bridge. // When user OTel is disabled, we force file exporter to /dev/null so the SDK // creates OtelSessionTracker (for debug panel) but doesn't export to any collector. @@ -210,6 +210,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS return new internal.LocalSessionManager({ featureFlagService: createLocalFeatureFlagService(), telemetryService: new internal.NoopTelemetryService(), + autoModeManager: new AutoModeSessionManager(), }, { flushDebounceMs: undefined, settings: undefined, version: undefined }); } catch (error) { @@ -221,8 +222,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS } private async getSDKPackage() { - const { internal, LocalSession, createLocalFeatureFlagService } = await this.copilotCLISDK.getPackage(); - return { internal, LocalSession, createLocalFeatureFlagService }; + const { internal, LocalSession, createLocalFeatureFlagService, AutoModeSessionManager } = await this.copilotCLISDK.getPackage(); + return { internal, LocalSession, createLocalFeatureFlagService, AutoModeSessionManager }; } getSessionWorkingDirectory(sessionId: string): Uri | undefined { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts index d5018b35e1751..f41992be205e9 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts @@ -15,9 +15,11 @@ import { NullChatDebugFileLoggerService } from '../../../../../platform/chat/com import { IConfigurationService } from '../../../../../platform/configuration/common/configurationService'; import { NullNativeEnvService } from '../../../../../platform/env/common/nullEnvService'; import { MockFileSystemService } from '../../../../../platform/filesystem/node/test/mockFileSystemService'; +import { MockGitService } from '../../../../../platform/ignore/node/test/mockGitService'; import { ILogService } from '../../../../../platform/log/common/logService'; import { NullMcpService } from '../../../../../platform/mcp/common/mcpService'; import { NoopOTelService, resolveOTelConfig } from '../../../../../platform/otel/common/index'; +import { MockPromptsService } from '../../../../../platform/promptFiles/test/common/mockPromptsService'; import { NullRequestLogger } from '../../../../../platform/requestLogger/node/nullRequestLogger'; import { NullWorkspaceService } from '../../../../../platform/workspace/common/workspaceService'; import { mock } from '../../../../../util/common/test/simpleMock'; @@ -41,8 +43,6 @@ import { CopilotCLISessionService, CopilotCLISessionWorkspaceTracker, ICopilotCL import { CopilotCLIMCPHandler } from '../mcpHandler'; import { IQuestion, IQuestionAnswer, IUserQuestionHandler } from '../userInputHelpers'; import { MockCliSdkSession, MockCliSdkSessionManager, MockSkillLocations, NullCopilotCLIAgents, NullICopilotCLIImageSupport } from './testHelpers'; -import { MockPromptsService } from '../../../../../platform/promptFiles/test/common/mockPromptsService'; -import { MockGitService } from '../../../../../platform/ignore/node/test/mockGitService'; // Re-export for backward compatibility with other spec files export { MockCliSdkSession, MockCliSdkSessionManager, MockSkillLocations, NullCopilotCLIAgents, NullICopilotCLIImageSupport } from './testHelpers'; @@ -108,7 +108,7 @@ describe('CopilotCLISessionService', () => { beforeEach(async () => { vi.useRealTimers(); const sdk = { - getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), noopTelemetryBinder: {} })), + getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), AutoModeSessionManager: class { }, noopTelemetryBinder: {} })), getRequestId: vi.fn(() => undefined), } as unknown as ICopilotCLISDK; @@ -341,7 +341,7 @@ describe('CopilotCLISessionService', () => { const sessionDir = URI.file(getCopilotCLISessionDir(sessionId)); const fileSystem = new MockFileSystemService(); const sdk = { - getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), noopTelemetryBinder: {} })) + getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), AutoModeSessionManager: class { }, noopTelemetryBinder: {} })) } as unknown as ICopilotCLISDK; const services = createExtensionUnitTestingServices(); disposables.add(services); @@ -380,7 +380,7 @@ describe('CopilotCLISessionService', () => { const sessionDir = URI.file(getCopilotCLISessionDir(sessionId)); const fileSystem = new MockFileSystemService(); const sdk = { - getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), noopTelemetryBinder: {} })) + getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), AutoModeSessionManager: class { }, noopTelemetryBinder: {} })) } as unknown as ICopilotCLISDK; const services = createExtensionUnitTestingServices(); disposables.add(services); @@ -445,7 +445,7 @@ describe('CopilotCLISessionService', () => { const sessionDir = URI.file(getCopilotCLISessionDir(sessionId)); const fileSystem = new MockFileSystemService(); const sdk = { - getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), noopTelemetryBinder: {} })) + getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), AutoModeSessionManager: class { }, noopTelemetryBinder: {} })) } as unknown as ICopilotCLISDK; const services = createExtensionUnitTestingServices(); disposables.add(services); @@ -491,7 +491,7 @@ describe('CopilotCLISessionService', () => { const sessionDir = URI.file(getCopilotCLISessionDir(sessionId)); const fileSystem = new MockFileSystemService(); const sdk = { - getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), noopTelemetryBinder: {} })) + getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), AutoModeSessionManager: class { }, noopTelemetryBinder: {} })) } as unknown as ICopilotCLISDK; const services = createExtensionUnitTestingServices(); disposables.add(services); @@ -755,7 +755,7 @@ describe('CopilotCLISessionService', () => { const storeMetadataSpy = vi.spyOn(metadataStore, 'storeForkedSessionMetadata'); const sdk = { - getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), noopTelemetryBinder: {} })), + getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), AutoModeSessionManager: class { }, noopTelemetryBinder: {} })), getRequestId: vi.fn(() => undefined), } as unknown as ICopilotCLISDK; const services = disposables.add(createExtensionUnitTestingServices()); @@ -796,7 +796,7 @@ describe('CopilotCLISessionService', () => { manager.sessions.set(sourceId, sdkSession); const sdk = { - getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), noopTelemetryBinder: {} })), + getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, LocalSession: MockLocalSession, createLocalFeatureFlagService: () => ({}), AutoModeSessionManager: class { }, noopTelemetryBinder: {} })), getRequestId: vi.fn(() => ({ vscodeRequestId: 'vsc-req-1', copilotRequestId: 'sdk-event-1' })), } as unknown as ICopilotCLISDK; const services = disposables.add(createExtensionUnitTestingServices()); diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts index 0279cc044e156..e190394cec3e1 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts @@ -327,7 +327,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { } }); sdk = { - getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, createLocalFeatureFlagService: () => ({}), noopTelemetryBinder: {} })), + getPackage: vi.fn(async () => ({ internal: { LocalSessionManager: MockCliSdkSessionManager, NoopTelemetryService: class { } }, createLocalFeatureFlagService: () => ({}), AutoModeSessionManager: class { }, noopTelemetryBinder: {} })), getAuthInfo: vi.fn(async () => ({ type: 'token' as const, token: 'valid-token', host: 'https://github.com' })), } as unknown as ICopilotCLISDK; const services = disposables.add(createExtensionUnitTestingServices()); diff --git a/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts b/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts index c55cc7143a354..f077dba256401 100644 --- a/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts +++ b/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts @@ -305,6 +305,8 @@ class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @IToolsService private readonly _toolsService: IToolsService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IExperimentationService private readonly _experimentationService: IExperimentationService, ) { } async executeEdit(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise { @@ -455,6 +457,12 @@ class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { userInitiatedRequest: true, location: ChatLocation.Editor, requestOptions, + modelCapabilities: { + enableThinking: this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatEnableThinking, this._experimentationService), + reasoningEffort: typeof request.modelConfiguration?.reasoningEffort === 'string' + ? request.modelConfiguration.reasoningEffort + : this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatReasoningEffort, this._experimentationService), + }, telemetryProperties: { messageId: telemetry.telemetryMessageId, conversationId: telemetry.sessionId, @@ -603,6 +611,8 @@ class InlineChatEditHeuristicStrategy implements IInlineChatEditStrategy { constructor( private readonly _intent: InlineChatIntent, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IExperimentationService private readonly _experimentationService: IExperimentationService, ) { } async executeEdit(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise { @@ -651,6 +661,12 @@ class InlineChatEditHeuristicStrategy implements IInlineChatEditStrategy { messages: renderResult.messages, userInitiatedRequest: true, location: ChatLocation.Editor, + modelCapabilities: { + enableThinking: this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatEnableThinking, this._experimentationService), + reasoningEffort: typeof request.modelConfiguration?.reasoningEffort === 'string' + ? request.modelConfiguration.reasoningEffort + : this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatReasoningEffort, this._experimentationService), + }, telemetryProperties: { messageId: telemetry.telemetryMessageId, conversationId: telemetry.sessionId, diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 9f1a1737e02ca..9e585d981b1b9 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -640,6 +640,8 @@ export namespace ConfigKey { export const UseAlternativeNESNotebookFormat = defineAndMigrateExpSetting('chat.advanced.notebook.alternativeNESFormat.enabled', 'chat.notebook.alternativeNESFormat.enabled', false); export const InlineChatSelectionRatioThreshold = defineSetting('chat.inlineChat.selectionRatioThreshold', ConfigType.ExperimentBased, 0); + export const InlineChatReasoningEffort = defineSetting('chat.inlineChat.reasoningEffort', ConfigType.ExperimentBased, 'low'); + export const InlineChatEnableThinking = defineSetting('chat.inlineChat.enableThinking', ConfigType.ExperimentBased, false); export const InstantApplyShortModelName = defineAndMigrateExpSetting('chat.advanced.instantApply.shortContextModelName', 'chat.instantApply.shortContextModelName', CHAT_MODEL.SHORT_INSTANT_APPLY); export const InstantApplyShortContextLimit = defineAndMigrateExpSetting('chat.advanced.instantApply.shortContextLimit', 'chat.instantApply.shortContextLimit', 8000); diff --git a/extensions/copilot/src/platform/endpoint/node/responsesApi.ts b/extensions/copilot/src/platform/endpoint/node/responsesApi.ts index c18488dcbe262..93729f900a9ff 100644 --- a/extensions/copilot/src/platform/endpoint/node/responsesApi.ts +++ b/extensions/copilot/src/platform/endpoint/node/responsesApi.ts @@ -86,8 +86,9 @@ export function createResponsesRequestBody(accessor: ServicesAccessor, options: body.truncation = configService.getConfig(ConfigKey.Advanced.UseResponsesApiTruncation) ? 'auto' : 'disabled'; + const thinkingExplicitlyDisabled = options.modelCapabilities?.enableThinking === false; const summaryConfig = configService.getExperimentBasedConfig(ConfigKey.ResponsesApiReasoningSummary, expService); - const shouldDisableReasoningSummary = endpoint.family === 'gpt-5.3-codex-spark-preview'; + const shouldDisableReasoningSummary = endpoint.family === 'gpt-5.3-codex-spark-preview' || thinkingExplicitlyDisabled; const effortFromSetting = configService.getConfig(ConfigKey.Advanced.ReasoningEffortOverride); const effort = endpoint.supportsReasoningEffort?.length ? (effortFromSetting || options.modelCapabilities?.reasoningEffort || 'medium') diff --git a/extensions/git/package.json b/extensions/git/package.json index 0af7ea212689b..17ddbced72d93 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2770,7 +2770,7 @@ { "command": "git.openChange", "group": "navigation@2", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && scmActiveResourceHasChanges" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && scmActiveResourceHasChanges && !isSessionsWindow" }, { "command": "git.stashApplyEditor", @@ -2786,37 +2786,37 @@ { "command": "git.stage", "group": "2_git@1", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasUnstagedChanges" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasUnstagedChanges && !isSessionsWindow" }, { "command": "git.unstage", "group": "2_git@2", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasStagedChanges" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasStagedChanges && !isSessionsWindow" }, { "command": "git.stage", "group": "2_git@1", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file && !isSessionsWindow" }, { "command": "git.stageSelectedRanges", "group": "2_git@2", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file && !isSessionsWindow" }, { "command": "git.unstage", "group": "2_git@3", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git && !isSessionsWindow" }, { "command": "git.unstageSelectedRanges", "group": "2_git@4", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git && !isSessionsWindow" }, { "command": "git.revertSelectedRanges", "group": "2_git@5", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file && !isSessionsWindow" } ], "editor/context": [ diff --git a/extensions/theme-defaults/themes/2026-dark.json b/extensions/theme-defaults/themes/2026-dark.json index a61ac87f4cc77..0561134ddad08 100644 --- a/extensions/theme-defaults/themes/2026-dark.json +++ b/extensions/theme-defaults/themes/2026-dark.json @@ -258,6 +258,9 @@ "gauge.errorBackground": "#F287724D", "chat.requestBubbleBackground": "#ffffff13", "chat.requestBubbleHoverBackground": "#ffffff22", + "chat.inputWorkingBorderColor1": "#9560D8", + "chat.inputWorkingBorderColor2": "#5BC25B", + "chat.inputWorkingBorderColor3": "#4A8FF5", "editorCommentsWidget.rangeBackground": "#488FAE26", "editorCommentsWidget.rangeActiveBackground": "#488FAE46", "charts.foreground": "#CCCCCC", diff --git a/extensions/theme-defaults/themes/2026-light.json b/extensions/theme-defaults/themes/2026-light.json index a49294ee42ad6..1b24ca3a52ded 100644 --- a/extensions/theme-defaults/themes/2026-light.json +++ b/extensions/theme-defaults/themes/2026-light.json @@ -261,6 +261,9 @@ "chat.requestBubbleBackground": "#EEF4FB", "chat.requestBubbleHoverBackground": "#E6EDFA", "chat.thinkingShimmer": "#999999", + "chat.inputWorkingBorderColor1": "#9B30FF", + "chat.inputWorkingBorderColor2": "#00C853", + "chat.inputWorkingBorderColor3": "#0044FF", "editorCommentsWidget.rangeBackground": "#EEF4FB", "editorCommentsWidget.rangeActiveBackground": "#E6EDFA", "charts.foreground": "#202020", diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 5e41bb11c157d..4077e9cadc5a5 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -119,21 +119,30 @@ export class EditorMouseEvent extends StandardMouseEvent { /** * Editor's coordinates relative to the whole document. */ - public readonly editorPos: EditorPagePosition; + public get editorPos(): EditorPagePosition { + this._editorPos ??= createEditorPagePosition(this._editorViewDomNode); + return this._editorPos; + } + private _editorPos: EditorPagePosition | undefined; /** * Coordinates relative to the (top;left) of the editor. * *NOTE*: These coordinates are preferred because they take into account transformations applied to the editor. * *NOTE*: These coordinates could be negative if the mouse position is outside the editor. - */ - public readonly relativePos: CoordinatesRelativeToEditor; + */ + public get relativePos(): CoordinatesRelativeToEditor { + this._relativePos ??= createCoordinatesRelativeToEditor(this._editorViewDomNode, this.editorPos, this.pos); + return this._relativePos; + } + private _relativePos: CoordinatesRelativeToEditor | undefined; + + private readonly _editorViewDomNode: HTMLElement; constructor(e: MouseEvent, isFromPointerCapture: boolean, editorViewDomNode: HTMLElement) { super(dom.getWindow(editorViewDomNode), e); this.isFromPointerCapture = isFromPointerCapture; this.pos = new PageCoordinates(this.posx, this.posy); - this.editorPos = createEditorPagePosition(editorViewDomNode); - this.relativePos = createCoordinatesRelativeToEditor(editorViewDomNode, this.editorPos, this.pos); + this._editorViewDomNode = editorViewDomNode; } } diff --git a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts index 82e6ff581bad9..87fd04735db76 100644 --- a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts +++ b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -3,16 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionWidgetService } from './actionWidget.js'; -import { IAction } from '../../../base/common/actions.js'; +import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js'; import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js'; -import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListItemHover, IActionListOptions } from './actionList.js'; -import { ThemeIcon } from '../../../base/common/themables.js'; +import { IListAccessibilityProvider } from '../../../base/browser/ui/list/listWidget.js'; +import { IAction } from '../../../base/common/actions.js'; import { Codicon } from '../../../base/common/codicons.js'; -import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js'; +import { ResolvedKeybinding } from '../../../base/common/keybindings.js'; +import { ThemeIcon } from '../../../base/common/themables.js'; import { IKeybindingService } from '../../keybinding/common/keybinding.js'; -import { IListAccessibilityProvider } from '../../../base/browser/ui/list/listWidget.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListItemHover, IActionListOptions } from './actionList.js'; +import { IActionWidgetService } from './actionWidget.js'; export interface IActionWidgetDropdownAction extends IAction { category?: { label: string; order: number; showHeader?: boolean }; @@ -26,6 +27,13 @@ export interface IActionWidgetDropdownAction extends IAction { * Optional toolbar actions shown when the item is focused or hovered. */ toolbarActions?: IAction[]; + /** + * Optional keybinding to display next to the action. When provided, this overrides the + * keybinding that would otherwise be looked up via {@link IKeybindingService.lookupKeybinding}. + * Useful when the active keybinding depends on a scoped context (e.g. focus state) that the + * dropdown cannot evaluate at display time. + */ + keybinding?: ResolvedKeybinding; } // TODO @lramos15 - Should we just make IActionProvider templated? @@ -139,7 +147,7 @@ export class ActionWidgetDropdown extends BaseDropdown { hideIcon: false, label: action.label, keybinding: this._options.showItemKeybindings ? - this.keybindingService.lookupKeybinding(action.id) : + (action.keybinding ?? this.keybindingService.lookupKeybinding(action.id)) : undefined, }); } diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index 53f46f7cf228f..60f72545d8ea2 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -387,8 +387,6 @@ border-color: var(--vscode-agentsChatInput-border) !important; background-color: var(--vscode-agentsChatInput-background); color: var(--vscode-agentsChatInput-foreground); - /* Preserve the agents-app input background under the developer-joy ring. */ - --chat-input-working-fill: var(--vscode-agentsChatInput-background); } .agent-sessions-workbench .interactive-session .chat-input-container.focused { diff --git a/src/vs/sessions/browser/parts/media/editorPart.css b/src/vs/sessions/browser/parts/media/editorPart.css index 841b364322f49..feb1ff1b7cf2f 100644 --- a/src/vs/sessions/browser/parts/media/editorPart.css +++ b/src/vs/sessions/browser/parts/media/editorPart.css @@ -63,3 +63,9 @@ /* When multiple tab bars are visible, only show editor actions for the last tab bar */ display: none; } + +/* Modal Editor Part */ + +.agent-sessions-workbench .part.editor.modal-editor-part .modal-editor-action-container .action-label.codicon.codicon-open-in-window { + transform: rotate(180deg); +} diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 437671338da63..6306bae337ba8 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -61,7 +61,7 @@ import { IMarkdownRendererService } from '../../platform/markdown/browser/markdo import { EditorMarkdownCodeBlockRenderer } from '../../editor/browser/widget/markdownRenderer/browser/editorMarkdownCodeBlockRenderer.js'; import { SyncDescriptor } from '../../platform/instantiation/common/descriptors.js'; import { TitleService } from './parts/titlebarPart.js'; -import { SessionsExperimentalShellGradientBackgroundSettingId } from '../common/configuration.js'; +import { SessionsExperimentalSendButtonGradientSettingId, SessionsExperimentalShellGradientBackgroundSettingId } from '../common/configuration.js'; import { IContextKeyService } from '../../platform/contextkey/common/contextkey.js'; import { EditorMaximizedContext } from '../common/contextkeys.js'; @@ -86,6 +86,7 @@ enum LayoutClasses { CHATBAR_HIDDEN = 'nochatbar', STATUSBAR_HIDDEN = 'nostatusbar', EXPERIMENTAL_SHELL_GRADIENT_BACKGROUND = 'experimental-shell-gradient-background', + EXPERIMENTAL_SEND_BUTTON_GRADIENT = 'sessions-experimental-send-button-gradient', FULLSCREEN = 'fullscreen', MAXIMIZED = 'maximized' } @@ -441,6 +442,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // Configuration changes this._register(configurationService.onDidChangeConfiguration(e => this.updateFontAliasing(e, configurationService))); this._register(configurationService.onDidChangeConfiguration(e => this.updateShellGradientBackground(e, configurationService))); + this._register(configurationService.onDidChangeConfiguration(e => this.updateSendButtonGradient(e, configurationService))); // Font Info if (isNative) { @@ -535,6 +537,17 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic ); } + private updateSendButtonGradient(e: IConfigurationChangeEvent | undefined, configurationService: IConfigurationService): void { + if (e && !e.affectsConfiguration(SessionsExperimentalSendButtonGradientSettingId)) { + return; + } + + this.mainContainer.classList.toggle( + LayoutClasses.EXPERIMENTAL_SEND_BUTTON_GRADIENT, + configurationService.getValue(SessionsExperimentalSendButtonGradientSettingId) + ); + } + //#endregion private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void { @@ -559,6 +572,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // Apply font aliasing this.updateFontAliasing(undefined, configurationService); this.updateShellGradientBackground(undefined, configurationService); + this.updateSendButtonGradient(undefined, configurationService); // Warm up font cache information before building up too many dom elements this.restoreFontInfo(storageService, configurationService); diff --git a/src/vs/sessions/common/configuration.ts b/src/vs/sessions/common/configuration.ts index 16949ea8c9c0d..920a0f37133c8 100644 --- a/src/vs/sessions/common/configuration.ts +++ b/src/vs/sessions/common/configuration.ts @@ -4,3 +4,4 @@ *--------------------------------------------------------------------------------------------*/ export const SessionsExperimentalShellGradientBackgroundSettingId = 'sessions.experimental.shellGradientBackground'; +export const SessionsExperimentalSendButtonGradientSettingId = 'sessions.experimental.sendButtonGradient'; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts index cf0cc288886fe..44845f688be43 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts @@ -24,6 +24,7 @@ import { IChatEditingService } from '../../../../workbench/contrib/chat/common/e import { ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; import { getSessionEditorComments } from './sessionEditorComments.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; export const submitFeedbackActionId = 'agentFeedbackEditor.action.submit'; export const navigatePreviousFeedbackActionId = 'agentFeedbackEditor.action.navigatePrevious'; @@ -208,6 +209,7 @@ class SubmitActiveSessionFeedbackAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const sessionManagementService = accessor.get(ISessionsManagementService); + const configurationService = accessor.get(IConfigurationService); const agentFeedbackService = accessor.get(IAgentFeedbackService); const chatWidgetService = accessor.get(IChatWidgetService); const editorService = accessor.get(IEditorService); @@ -231,18 +233,20 @@ class SubmitActiveSessionFeedbackAction extends Action2 { } // Close all editors belonging to the session resource - const editorsToClose: IEditorIdentifier[] = []; - for (const { editor, groupId } of editorService.getEditors(EditorsOrder.SEQUENTIAL)) { - const candidates = getActiveResourceCandidates(editor); - const belongsToSession = candidates.some(uri => - isEqual(agentFeedbackService.getMostRecentSessionForResource(uri), sessionResource) - ); - if (belongsToSession) { - editorsToClose.push({ editor, groupId }); + if (configurationService.getValue('workbench.editor.useModal') === 'all') { + const editorsToClose: IEditorIdentifier[] = []; + for (const { editor, groupId } of editorService.getEditors(EditorsOrder.SEQUENTIAL)) { + const candidates = getActiveResourceCandidates(editor); + const belongsToSession = candidates.some(uri => + isEqual(agentFeedbackService.getMostRecentSessionForResource(uri), sessionResource) + ); + if (belongsToSession) { + editorsToClose.push({ editor, groupId }); + } + } + if (editorsToClose.length) { + await editorService.closeEditors(editorsToClose); } - } - if (editorsToClose.length) { - await editorService.closeEditors(editorsToClose); } await widget.acceptInput('/act-on-feedback'); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts index c3e6cca371f00..fd3266f8d9388 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts @@ -12,8 +12,10 @@ import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMa import { EditorResourceAccessor, SideBySideEditor } from '../../../../workbench/common/editor.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js'; -import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; +import { MultiDiffEditorInput } from '../../../../workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.js'; +import { ISessionFileChange } from '../../../services/sessions/common/session.js'; /** * Find the session that contains the given resource by checking editing sessions, @@ -39,14 +41,12 @@ export function getSessionForResource( return undefined; } -export type AgentFeedbackSessionChange = IChatSessionFileChange | IChatSessionFileChange2; - export interface IAgentFeedbackContext { readonly codeSelection?: string; readonly diffHunks?: string; } -export function changeMatchesResource(change: AgentFeedbackSessionChange, resourceUri: URI): boolean { +export function changeMatchesResource(change: ISessionFileChange, resourceUri: URI): boolean { if (isIChatSessionFileChange2(change)) { return change.uri.fsPath === resourceUri.fsPath || change.modifiedUri?.fsPath === resourceUri.fsPath @@ -61,7 +61,7 @@ export function getSessionChangeForResource( sessionResource: URI | undefined, resourceUri: URI, sessionsManagementService: ISessionsManagementService, -): AgentFeedbackSessionChange | undefined { +): ISessionFileChange | undefined { if (!sessionResource) { return undefined; } @@ -315,6 +315,18 @@ function renderHunkGroup( export function getActiveResourceCandidates(input: Parameters[0]): URI[] { const result: URI[] = []; + + if (input instanceof MultiDiffEditorInput) { + const items = input.resources.get(); + if (items) { + for (const item of items) { + if (item.originalUri) { result.push(item.originalUri); } + if (item.modifiedUri) { result.push(item.modifiedUri); } + } + } + return result; + } + const resources = EditorResourceAccessor.getOriginalUri(input, { supportSideBySide: SideBySideEditor.BOTH }); if (!resources) { return result; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts index 3c61bfec655f2..66234c9fc6c65 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts @@ -26,7 +26,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import * as nls from '../../../../nls.js'; import { IAgentFeedbackService } from './agentFeedbackService.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; -import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { createAgentFeedbackContext, getSessionForResource } from './agentFeedbackEditorUtils.js'; import { ICodeReviewService, IPRReviewState } from '../../codeReview/browser/codeReviewService.js'; @@ -37,6 +37,7 @@ import { IMarkdownRendererService } from '../../../../platform/markdown/browser/ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { ISessionFileChange } from '../../../services/sessions/common/session.js'; interface ICommentItemActions { editAction: Action; @@ -730,7 +731,7 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito return comments.filter(comment => comment.resourceUri.fsPath === resourceUri.fsPath); } - private _getSessionChangeForResource(resourceUri: URI): IChatSessionFileChange | IChatSessionFileChange2 | undefined { + private _getSessionChangeForResource(resourceUri: URI): ISessionFileChange | undefined { if (!this._sessionResource) { return undefined; } @@ -743,7 +744,7 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito return changes.find(change => this._changeMatchesFsPath(change, resourceUri)); } - private _changeMatchesFsPath(change: IChatSessionFileChange | IChatSessionFileChange2, resourceUri: URI): boolean { + private _changeMatchesFsPath(change: ISessionFileChange, resourceUri: URI): boolean { if (isIChatSessionFileChange2(change)) { return change.uri.fsPath === resourceUri.fsPath || change.modifiedUri?.fsPath === resourceUri.fsPath @@ -754,7 +755,7 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito || change.originalUri?.fsPath === resourceUri.fsPath; } - private _isCurrentOrModifiedResource(change: IChatSessionFileChange | IChatSessionFileChange2, resourceUri: URI): boolean { + private _isCurrentOrModifiedResource(change: ISessionFileChange, resourceUri: URI): boolean { if (isIChatSessionFileChange2(change)) { return isEqual(change.uri, resourceUri) || (change.modifiedUri ? isEqual(change.modifiedUri, resourceUri) : false); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 4c0b1ec7b8c98..8bafcf1c8657b 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -11,7 +11,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { generateUuid } from '../../../../base/common/uuid.js'; import { isEqual } from '../../../../base/common/resources.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; -import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js'; import { changeMatchesResource, IAgentFeedbackContext } from './agentFeedbackEditorUtils.js'; @@ -22,6 +22,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { ICodeReviewSuggestion } from '../../codeReview/browser/codeReviewService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { logChangesViewReviewCommentAdded } from '../../../common/sessionsTelemetry.js'; +import { ISessionFileChange } from '../../../services/sessions/common/session.js'; // --- Types -------------------------------------------------------------------- @@ -363,11 +364,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe this.setNavigationAnchor(sessionResource, commentId); } - private _getSessionChange(resourceUri: URI, changes: readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[] | { - readonly files: number; - readonly insertions: number; - readonly deletions: number; - } | undefined): { originalUri?: URI; modifiedUri: URI; isDeletion: boolean } | undefined { + private _getSessionChange(resourceUri: URI, changes: readonly ISessionFileChange[] | undefined): { originalUri?: URI; modifiedUri: URI; isDeletion: boolean } | undefined { if (!(changes instanceof Array)) { return undefined; } @@ -392,7 +389,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe }; } - private _changeContainsResource(change: IChatSessionFileChange | IChatSessionFileChange2, resourceUri: URI): boolean { + private _changeContainsResource(change: ISessionFileChange, resourceUri: URI): boolean { if (isIChatSessionFileChange2(change)) { return change.uri.fsPath === resourceUri.fsPath || change.originalUri?.fsPath === resourceUri.fsPath diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts index 279c2d94a5b32..a8dac0e1c4bfd 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts @@ -22,7 +22,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +import { AICustomizationManagementSection, AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; import { agentIcon, instructionsIcon, mcpServerIcon, pluginIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -35,6 +35,10 @@ const $ = DOM.$; export const AI_CUSTOMIZATION_OVERVIEW_VIEW_ID = 'workbench.view.aiCustomizationOverview'; +function isWelcomePageEditor(editor: unknown): editor is { showWelcomePage(): void } { + return typeof (editor as { showWelcomePage?: unknown })?.showWelcomePage === 'function'; +} + interface ISectionSummary { readonly id: AICustomizationManagementSection; readonly label: string; @@ -222,7 +226,13 @@ export class AICustomizationOverviewView extends ViewPane { private async openOverview(): Promise { const input = AICustomizationManagementEditorInput.getOrCreate(); - await this.editorService.openEditor(input, { pinned: true }); + const editor = await this.editorService.openEditor(input, { pinned: true }); + + // Always reset to the welcome page when opening from the sidebar, + // so we don't restore the previously selected section. + if (editor?.getId() === AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID && isWelcomePageEditor(editor)) { + editor.showWelcomePage(); + } } protected override layoutBody(height: number, width: number): void { diff --git a/src/vs/sessions/contrib/changes/browser/changesTitleBarWidget.ts b/src/vs/sessions/contrib/changes/browser/changesTitleBarWidget.ts index 17842699f6046..4a8c8d9a000ee 100644 --- a/src/vs/sessions/contrib/changes/browser/changesTitleBarWidget.ts +++ b/src/vs/sessions/contrib/changes/browser/changesTitleBarWidget.ts @@ -22,7 +22,6 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { IsAuxiliaryWindowContext, AuxiliaryBarVisibleContext } from '../../../../workbench/common/contextkeys.js'; -import { getAgentChangesSummary } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; import { IPaneCompositePartService } from '../../../../workbench/services/panecomposite/browser/panecomposite.js'; import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; @@ -33,6 +32,7 @@ import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; import { logChangesViewToggle } from '../../../common/sessionsTelemetry.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { CHANGES_VIEW_CONTAINER_ID } from '../common/changes.js'; +import { ISessionFileChange } from '../../../services/sessions/common/session.js'; const TOGGLE_CHANGES_VIEW_ID = 'workbench.action.agentSessions.toggleChangesView'; @@ -106,7 +106,7 @@ class ChangesTitleBarActionViewItem extends BaseActionViewItem { const activeSession = this.activeSessionService.activeSession.get(); const resource = activeSession?.resource; const session = resource ? this.activeSessionService.getSession(resource) : undefined; - const summary = session ? getAgentChangesSummary(session.changes.get()) : undefined; + const summary = session ? this._getSessionChangesSummary(session.changes.get()) : undefined; // Rebuild inner content: [diff icon] +insertions -deletions append(btn, $(ThemeIcon.asCSSSelector(Codicon.diffMultiple))); @@ -135,6 +135,22 @@ class ChangesTitleBarActionViewItem extends BaseActionViewItem { )); } } + + private _getSessionChangesSummary(changes: readonly ISessionFileChange[]): { + files: number; insertions: number; deletions: number; + } | undefined { + if (changes.length === 0) { + return undefined; + } + + let insertions = 0, deletions = 0; + for (const change of changes) { + insertions += change.insertions; + deletions += change.deletions; + } + + return { files: changes.length, insertions, deletions }; + } } /** diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 60b1b833a5ade..4c5f2f1bfb57c 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -59,7 +59,7 @@ import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/b import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService, PRReviewStateKind } from '../../codeReview/browser/codeReviewService.js'; import { CIStatusWidget } from './checksWidget.js'; -import { COPILOT_CLOUD_SESSION_TYPE, GITHUB_REMOTE_FILE_SCHEME, SessionStatus } from '../../../services/sessions/common/session.js'; +import { COPILOT_CLOUD_SESSION_TYPE, GITHUB_REMOTE_FILE_SCHEME, ISessionFileChange, SessionStatus } from '../../../services/sessions/common/session.js'; import { Orientation } from '../../../../base/browser/ui/sash/sash.js'; import { IView, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js'; import { Color } from '../../../../base/common/color.js'; @@ -74,7 +74,7 @@ import { ResourceTree } from '../../../../base/common/resourceTree.js'; import { structuralEquals } from '../../../../base/common/equals.js'; import { compareFileNames, comparePaths } from '../../../../base/common/comparers.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; const $ = dom.$; @@ -943,7 +943,7 @@ export class ChangesViewPane extends ViewPane { )); } - async openChanges(): Promise { + async openChanges(resource?: URI): Promise { const items = this.viewModel.activeSessionChangesObs.get(); if (items.length === 0) { return; @@ -952,12 +952,13 @@ export class ChangesViewPane extends ViewPane { const modalEditorMode = this.configurationService.getValue('workbench.editor.useModal'); if (modalEditorMode === 'all') { const changes = toIChangesFileItem(items); - await this._openFileItem(changes[0], changes, false, false, false, changes.length > 1); + const changeToOpen = resource ? changes.find(c => isEqual(c.uri, resource)) : undefined; + await this._openFileItem(changeToOpen ?? changes[0], changes, false, false, false, changes.length > 1); return; } // Open multi-file diff editor - await this._openMultiFileDiffEditor(); + await this._openMultiFileDiffEditor(resource); } private async _openFileItem(item: IChangesFileItem, items: IChangesFileItem[], sideBySide: boolean, preserveFocus: boolean, pinned: boolean, includeSidebar: boolean): Promise { @@ -1014,7 +1015,7 @@ export class ChangesViewPane extends ViewPane { return; } - const compare = (aChange: IChatSessionFileChange | IChatSessionFileChange2, bChange: IChatSessionFileChange | IChatSessionFileChange2): number => { + const compare = (aChange: ISessionFileChange, bChange: ISessionFileChange): number => { const aPath = isIChatSessionFileChange2(aChange) ? aChange.uri.fsPath : aChange.modifiedUri.fsPath; const bPath = isIChatSessionFileChange2(bChange) ? bChange.uri.fsPath : bChange.modifiedUri.fsPath; return comparePaths(aPath, bPath); diff --git a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts index 86ec33bab13fc..d802caaabd1c3 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts @@ -26,6 +26,9 @@ import { ViewContainerLocation } from '../../../../workbench/common/views.js'; import { ChangesViewPane } from './changesView.js'; import { SESSIONS_FILES_CONTAINER_ID } from '../../files/browser/files.contribution.js'; import { SESSIONS_FILES_VIEW_ID } from '../../files/browser/filesView.js'; +import { URI } from '../../../../base/common/uri.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; const openChangesViewActionOptions: IAction2Options = { id: 'workbench.action.agentSessions.openChangesView', @@ -197,3 +200,66 @@ class OpenPullRequestAction extends Action2 { } registerAction2(OpenPullRequestAction); + +class OpenFileAction extends Action2 { + static readonly ID = 'workbench.action.agentSessions.openFile'; + + constructor() { + super({ + id: OpenFileAction.ID, + title: localize2('openFile', "Open File"), + icon: Codicon.goToFile, + f1: false, + menu: { + id: MenuId.ChatEditingSessionChangeToolbar, + group: 'navigation', + order: 1, + when: IsSessionsWindowContext, + alt: { + id: 'workbench.action.agentSessions.openChanges', + title: localize2('openChanges', "Open Changes"), + icon: Codicon.gitCompare, + } + } + }); + } + + async run(accessor: ServicesAccessor, _sessionResource: URI, _ref: string, ...resources: URI[]): Promise { + const editorService = accessor.get(IEditorService); + await Promise.all(resources.map(resource => editorService.openEditor({ resource }))); + } +} + +registerAction2(OpenFileAction); + +class OpenChangesAction extends Action2 { + static readonly ID = 'workbench.action.agentSessions.openChanges'; + + constructor() { + super({ + id: OpenChangesAction.ID, + title: localize2('openChanges', "Open Changes"), + icon: Codicon.gitCompare, + f1: false + }); + } + + async run(accessor: ServicesAccessor, _sessionResource: URI, _ref: string, ...resources: URI[]): Promise { + const viewsService = accessor.get(IViewsService); + const editorService = accessor.get(IEditorService); + + const view = viewsService.getViewWithId(CHANGES_VIEW_ID); + const sessionChanges = view?.viewModel.activeSessionChangesObs.get(); + + const changes = sessionChanges?.filter(change => + resources.some(resource => isEqual(change.modifiedUri ?? change.originalUri, resource)) + ) ?? []; + + await Promise.all(changes.map(change => editorService.openEditor({ + original: { resource: change.originalUri }, + modified: { resource: change.modifiedUri } + }))); + } +} + +registerAction2(OpenChangesAction); diff --git a/src/vs/sessions/contrib/changes/browser/changesViewModel.ts b/src/vs/sessions/contrib/changes/browser/changesViewModel.ts index 5114a455fa932..d1b0e4a8715f5 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewModel.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewModel.ts @@ -13,9 +13,9 @@ import { isEqual } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { IChatSessionFileChange, IChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { IChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { GitDiffChange, IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; -import { COPILOT_CLOUD_SESSION_TYPE } from '../../../services/sessions/common/session.js'; +import { COPILOT_CLOUD_SESSION_TYPE, ISessionFileChange } from '../../../services/sessions/common/session.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { IAgentFeedbackService } from '../../agentFeedback/browser/agentFeedbackService.js'; import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService, PRReviewStateKind } from '../../codeReview/browser/codeReviewService.js'; @@ -78,7 +78,7 @@ export interface ActiveSessionState { export class ChangesViewModel extends Disposable { readonly activeSessionResourceObs: IObservable; readonly activeSessionTypeObs: IObservable; - readonly activeSessionChangesObs: IObservable; + readonly activeSessionChangesObs: IObservable; readonly activeSessionHasGitRepositoryObs: IObservable; readonly activeSessionFirstCheckpointRefObs: IObservable; readonly activeSessionLastCheckpointRefObs: IObservable; @@ -227,7 +227,7 @@ export class ChangesViewModel extends Disposable { }); } - private _getActiveSessionChanges(): IObservable { + private _getActiveSessionChanges(): IObservable { // Changes const activeSessionChangesObs = derived(reader => { const activeSession = this.sessionManagementService.activeSession.read(reader); @@ -318,7 +318,7 @@ export class ChangesViewModel extends Disposable { }); return derivedOpts({ - equalsFn: arrayEqualsC() + equalsFn: arrayEqualsC() }, reader => { const versionMode = this.versionModeObs.read(reader); diff --git a/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts b/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts index 46f71be3ce78a..cc31d1a26a639 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts @@ -23,16 +23,16 @@ import { ILabelService } from '../../../../platform/label/common/label.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IResourceLabel, ResourceLabels } from '../../../../workbench/browser/labels.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; -import { GITHUB_REMOTE_FILE_SCHEME } from '../../../services/sessions/common/session.js'; +import { GITHUB_REMOTE_FILE_SCHEME, ISessionFileChange } from '../../../services/sessions/common/session.js'; import { ActiveSessionContextKeys, ChangesContextKeys, ChangesViewMode } from '../common/changes.js'; import { ChangesViewModel } from './changesViewModel.js'; const $ = dom.$; -export function toIChangesFileItem(changes: readonly (IChatSessionFileChange | IChatSessionFileChange2)[]): IChangesFileItem[] { +export function toIChangesFileItem(changes: readonly ISessionFileChange[]): IChangesFileItem[] { return changes.map(change => { const isAddition = change.originalUri === undefined; const isDeletion = change.modifiedUri === undefined; diff --git a/src/vs/sessions/contrib/chat/browser/media/chatInput.css b/src/vs/sessions/contrib/chat/browser/media/chatInput.css index 60489450b87ec..00868b5b63b2e 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatInput.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatInput.css @@ -62,7 +62,7 @@ /* Height constraints are driven by MIN_EDITOR_HEIGHT / MAX_EDITOR_HEIGHT in newChatViewPane.ts */ .sessions-chat-editor { - padding: 6px 6px 0 6px; + padding: 2px 10px 0 10px; flex-shrink: 1; } @@ -185,11 +185,65 @@ gap: 4px; } +/* Delightful gradient styling for the chat send (submit) button. The button + gets a multi-color gradient ring at rest using the same palette as the + working-state border, fills with a slowly cycling color on hover/focus, + and emits a quick colorful pulse on click. Gated behind the experimental + `sessions.experimental.sendButtonGradient` setting via the + `.sessions-experimental-send-button-gradient` class on the workbench root. */ +@property --chat-send-button-anim-angle { + syntax: ''; + inherits: false; + initial-value: 135deg; +} + +@keyframes chat-send-button-spin { + from { + --chat-send-button-anim-angle: 135deg; + } + + to { + --chat-send-button-anim-angle: 495deg; + } +} + +@keyframes chat-send-button-color-cycle { + 0%, + 100% { + background-color: color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 60%, var(--vscode-input-background)); + } + + 33% { + background-color: color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor2) 60%, var(--vscode-input-background)); + } + + 66% { + background-color: color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor3) 60%, var(--vscode-input-background)); + } +} + +@keyframes chat-send-button-pulse { + 0% { + opacity: 0.7; + transform: scale(1); + } + + 100% { + opacity: 0; + transform: scale(1.3); + } +} + /* Send button - wraps a Button widget */ .sessions-chat-send-button { display: flex; align-items: center; + justify-content: center; flex-shrink: 0; + position: relative; + width: 22px; + height: 22px; + border-radius: 4px; } .sessions-chat-send-button .monaco-button { @@ -202,23 +256,229 @@ padding: 0; border-radius: 4px; color: var(--vscode-icon-foreground); - background: transparent !important; - border: none !important; + background: transparent; + border: none; cursor: pointer; + position: relative; + z-index: 1; + transition: background-color 250ms ease, color 250ms ease; } .sessions-chat-send-button .monaco-button.disabled { cursor: default; } +/* Default hover feedback when the gradient experiment is off. The gradient-on + rules below override this with the cycling color treatment. */ .sessions-chat-send-button .monaco-button:not(.disabled):hover { - background-color: var(--vscode-toolbar-hoverBackground) !important; + background-color: var(--vscode-toolbar-hoverBackground); +} + +/* Focus indicator drawn on the wrapper so it sits cleanly around the + 22x22 button surface (the inner Button widget doesn't draw its own + focus border). Works in both gradient-on and gradient-off states. */ +.sessions-chat-send-button:has(.monaco-button:not(.disabled):focus-visible) { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: 1px; +} + +/* Suppress the inner button's default focus outline so it doesn't double up + with the wrapper outline above. */ +.sessions-chat-send-button .monaco-button:focus, +.sessions-chat-send-button .monaco-button:focus-visible { + outline: none !important; + box-shadow: none !important; } .monaco-workbench .sessions-chat-send-button .monaco-button .codicon[class*='codicon-'] { font-size: 14px; } +/* Ensure no underline / link decoration ever shows under the codicon glyph + (the button is rendered as an tag). */ +.monaco-workbench .sessions-chat-send-button a.monaco-button, +.monaco-workbench .sessions-chat-send-button a.monaco-button:hover, +.monaco-workbench .sessions-chat-send-button a.monaco-button:focus, +.monaco-workbench .sessions-chat-send-button a.monaco-button:active, +.monaco-workbench .sessions-chat-send-button a.monaco-button.disabled { + text-decoration: none !important; +} + +/* Gradient ring at rest, drawn on the wrapper so the button's own + ::before (codicon glyph) is not overridden. Gated behind the experimental + `sessions.experimental.sendButtonGradient` setting via the + `.sessions-experimental-send-button-gradient` class on the workbench root. */ +.monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 4px; + padding: 1px; + background: conic-gradient(from var(--chat-send-button-anim-angle), + var(--vscode-chat-inputWorkingBorderColor1), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor3), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor1)); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask-composite: exclude; + pointer-events: none; + transition: opacity 250ms ease; + z-index: 2; + /* Idle: very slowly rotate the gradient ring. */ + animation: chat-send-button-spin 18s linear infinite; +} + +.monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button.disabled)::before { + display: none; +} + +/* Hover/focus: fill the button with a solid color that smoothly cycles through + the gradient palette, rather than spinning the conic gradient. Focus mirrors + hover so keyboard users get the same delightful treatment. */ +.monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button:not(.disabled):hover) .monaco-button, +.monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button:not(.disabled):focus-visible) .monaco-button { + background-color: color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 60%, var(--vscode-input-background)); + animation: chat-send-button-color-cycle 4.5s ease-in-out infinite; + color: var(--vscode-button-foreground); +} + +.monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button:not(.disabled):hover)::before, +.monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button:not(.disabled):focus-visible)::before { + opacity: 0; +} + +/* Click: outward color pulse on the wrapper. */ +.monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button:not(.disabled):active)::after { + content: ''; + position: absolute; + inset: -2px; + border-radius: 6px; + background: conic-gradient(from 135deg, + var(--vscode-chat-inputWorkingBorderColor1), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor3), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor1)); + pointer-events: none; + animation: chat-send-button-pulse 400ms ease-out forwards; + z-index: 0; +} + +/* ---------------------------------------------------------------------------- + Gradient styling for the standard chat-input send button (the one rendered + inside session views by the shared ChatInputPart). Mirrors the wrapper + rules above but targets the toolbar action-item that hosts the arrow-up + codicon. Gated by the same `.sessions-experimental-send-button-gradient` class on + the sessions workbench root so only Sessions/Agents UI is affected. + ---------------------------------------------------------------------------- */ +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up) { + position: relative; + border-radius: 5px; +} + +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up) > .action-label.codicon-arrow-up { + transition: background-color 250ms ease, color 250ms ease; +} + +/* Focus indicator drawn on the action-item wrapper so it sits cleanly around + the button surface with a small offset, matching the new-session button. */ +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:focus-visible) { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: 1px; + border-radius: 5px; +} + +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up) > .action-label.codicon-arrow-up:focus, +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up) > .action-label.codicon-arrow-up:focus-visible { + outline: none; +} + +/* Gradient ring at rest, drawn with the standard mask trick so only the + 1px border area is painted (icon stays transparent over toolbar). */ +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up)::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: conic-gradient(from var(--chat-send-button-anim-angle), + var(--vscode-chat-inputWorkingBorderColor1), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor3), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor1)); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask-composite: exclude; + pointer-events: none; + transition: opacity 250ms ease; + z-index: 1; + animation: chat-send-button-spin 18s linear infinite; +} + +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item.disabled:has(> .action-label.codicon-arrow-up)::before { + display: none; +} + +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled) > .action-label.codicon-arrow-up:hover, +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled) > .action-label.codicon-arrow-up:focus-visible { + background-color: color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 60%, var(--vscode-input-background)); + animation: chat-send-button-color-cycle 4.5s ease-in-out infinite; +} + +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:hover)::before, +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:focus-visible)::before { + opacity: 0; +} + +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:hover) .codicon-arrow-up, +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:focus-visible) .codicon-arrow-up { + color: var(--vscode-button-foreground) !important; +} + +.monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:active)::after { + content: ''; + position: absolute; + inset: -2px; + border-radius: 7px; + background: conic-gradient(from 135deg, + var(--vscode-chat-inputWorkingBorderColor1), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor3), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor1)); + pointer-events: none; + animation: chat-send-button-pulse 400ms ease-out forwards; + z-index: 0; +} + +@media (prefers-reduced-motion: reduce) { + /* New-session send button */ + .monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button::before, + .monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button:not(.disabled):hover) .monaco-button, + .monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button:not(.disabled):focus-visible) .monaco-button, + .monaco-workbench.sessions-experimental-send-button-gradient .sessions-chat-send-button:has(.monaco-button:not(.disabled):active)::after, + /* Existing-chat toolbar send button */ + .monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up)::before, + .monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled) > .action-label.codicon-arrow-up:hover, + .monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled) > .action-label.codicon-arrow-up:focus-visible, + .monaco-workbench.sessions-experimental-send-button-gradient .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:active)::after { + animation: none; + } +} + /* Loading spinner in toolbar */ .sessions-chat-loading-spinner { display: none; diff --git a/src/vs/sessions/contrib/chat/browser/newChatInput.ts b/src/vs/sessions/contrib/chat/browser/newChatInput.ts index 33d9d5fb003d8..60be30e47862c 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatInput.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatInput.ts @@ -59,6 +59,39 @@ interface IDraftState { attachments: readonly IChatRequestVariableEntry[]; } +/** + * Randomized, friendly placeholders shown in the new-session chat input + * to add a bit of personality. One is picked per widget instance, avoiding + * an immediate repeat of the previous pick. + */ +const RANDOM_PLACEHOLDERS = [ + localize('sessionsChatInput.placeholder.whatAreYouBuilding', "What are you building?"), + localize('sessionsChatInput.placeholder.whatWillYouShipToday', "What will you ship today?"), + localize('sessionsChatInput.placeholder.describeWhatYouWantToBuild', "Describe what you want to build"), + localize('sessionsChatInput.placeholder.whatsYourNextMilestone', "What's your next milestone?"), + localize('sessionsChatInput.placeholder.whatAreYouTryingToAchieve', "What are you trying to achieve?"), + localize('sessionsChatInput.placeholder.pitchYourIdea', "Pitch your idea"), + localize('sessionsChatInput.placeholder.whatsTheGoal', "What's the goal?"), + localize('sessionsChatInput.placeholder.whatWillYouCreate', "What will you create?"), + localize('sessionsChatInput.placeholder.whatFeatureAreYouDreamingUp', "What feature are you dreaming up?"), + localize('sessionsChatInput.placeholder.describeTheOutcome', "Describe the outcome you want"), + localize('sessionsChatInput.placeholder.whatProblemAreYouSolving', "What problem are you solving?"), + localize('sessionsChatInput.placeholder.whatsNextOnYourRoadmap', "What's next on your roadmap?"), + localize('sessionsChatInput.placeholder.whatWouldYouLikeToAutomate', "What would you like to automate?"), + localize('sessionsChatInput.placeholder.whatWillYouLaunch', "What will you launch?"), + localize('sessionsChatInput.placeholder.describeYourMission', "Describe your mission"), +]; + +let lastPlaceholderIndex = -1; +function getRandomChatInputPlaceholder(): string { + let index = Math.floor(Math.random() * RANDOM_PLACEHOLDERS.length); + if (index === lastPlaceholderIndex) { + index = (index + 1) % RANDOM_PLACEHOLDERS.length; + } + lastPlaceholderIndex = index; + return RANDOM_PLACEHOLDERS[index]; +} + // #region --- New Chat Widget --- export class NewChatInputWidget extends Disposable implements IHistoryNavigationWidget { @@ -232,7 +265,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation ...getSimpleEditorOptions(this.configurationService), readOnly: false, ariaLabel: this._getAriaLabel(), - placeholder: this.options.placeholder, + placeholder: this.options.placeholder ?? getRandomChatInputPlaceholder(), fontFamily: 'system-ui, -apple-system, sans-serif', fontSize: 13, lineHeight: 20, diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts index 2a4683c5ec4f7..4f5dde853c529 100644 --- a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts +++ b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts @@ -14,9 +14,10 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { generateUuid } from '../../../../base/common/uuid.js'; import { hash } from '../../../../base/common/hash.js'; import { hasKey } from '../../../../base/common/types.js'; -import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IGitHubService } from '../../github/browser/githubService.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; +import { ISessionFileChange } from '../../../services/sessions/common/session.js'; // --- Types ------------------------------------------------------------------- @@ -45,7 +46,7 @@ export interface ICodeReviewFile { readonly baseUri?: URI; } -export function getCodeReviewFilesFromSessionChanges(changes: readonly (IChatSessionFileChange | IChatSessionFileChange2)[]): readonly ICodeReviewFile[] { +export function getCodeReviewFilesFromSessionChanges(changes: readonly ISessionFileChange[]): readonly ICodeReviewFile[] { return changes.map(change => { if (isIChatSessionFileChange2(change)) { return { diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 3f1dc4f583187..a441cc2871b6e 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -6,7 +6,7 @@ import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { localize } from '../../../../nls.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { SessionsExperimentalShellGradientBackgroundSettingId } from '../../../common/configuration.js'; +import { SessionsExperimentalSendButtonGradientSettingId, SessionsExperimentalShellGradientBackgroundSettingId } from '../../../common/configuration.js'; import { ThemeSettingDefaults } from '../../../../workbench/services/themes/common/workbenchThemeService.js'; Registry.as(Extensions.Configuration).registerConfiguration({ @@ -19,6 +19,13 @@ Registry.as(Extensions.Configuration).registerConfigurat tags: ['experimental'], description: localize('sessions.experimental.shellGradientBackground', "Whether to enable the experimental accent-tinted shell background in the Sessions window."), }, + [SessionsExperimentalSendButtonGradientSettingId]: { + type: 'boolean', + default: false, + scope: ConfigurationScope.APPLICATION, + tags: ['experimental'], + description: localize('sessions.experimental.sendButtonGradient', "Whether to show a colorful animated gradient on the chat send button in the Sessions window. The button shows a slowly rotating gradient ring at rest, fills with a cycling color on hover, and emits a color pulse on click."), + }, }, }); diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts index c93edc1ff4999..4797b7946fc98 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts @@ -21,8 +21,8 @@ import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browse import { AgentSessionProviders, AgentSessionTarget } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { IChatService, IChatSendRequestOptions } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { IChatResponseModel } from '../../../../workbench/contrib/chat/common/model/chatModel.js'; -import { ChatSessionStatus, IChatSessionFileChange, IChatSessionsService, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; -import { ISession, IChat, ISessionRepository, ISessionWorkspace, SessionStatus, GITHUB_REMOTE_FILE_SCHEME, IGitHubInfo, CopilotCLISessionType, CopilotCloudSessionType, ClaudeCodeSessionType, ISessionType, ISessionWorkspaceBrowseAction } from '../../../services/sessions/common/session.js'; +import { ChatSessionStatus, IChatSessionsService, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { ISession, IChat, ISessionRepository, ISessionWorkspace, SessionStatus, GITHUB_REMOTE_FILE_SCHEME, IGitHubInfo, CopilotCLISessionType, CopilotCloudSessionType, ClaudeCodeSessionType, ISessionType, ISessionWorkspaceBrowseAction, ISessionFileChange } from '../../../services/sessions/common/session.js'; import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../../../../workbench/contrib/chat/common/constants.js'; import { basename, isEqual } from '../../../../base/common/resources.js'; import { ISendRequestOptions, ISessionChangeEvent, ISessionsProvider } from '../../../services/sessions/common/sessionsProvider.js'; @@ -70,7 +70,7 @@ export interface ICopilotChatSession { /** Current session status. */ readonly status: IObservable; /** File changes produced by the session. */ - readonly changes: IObservable; + readonly changes: IObservable; /** Currently selected model identifier. */ readonly modelId: IObservable; /** Currently selected mode identifier and kind. */ @@ -179,8 +179,8 @@ class CopilotCLISession extends Disposable implements ICopilotChatSession { private readonly _loading = observableValue(this, true); readonly loading: IObservable = this._loading; - private readonly _changes: ReturnType>; - readonly changes: IObservable; + private readonly _changes: ReturnType>; + readonly changes: IObservable; private readonly _isArchived = observableValue(this, false); readonly isArchived: IObservable = this._isArchived; @@ -258,7 +258,7 @@ class CopilotCLISession extends Disposable implements ICopilotChatSession { this._description = observableValue(this, undefined); this.description = this._description; - this._changes = observableValue(this, []); + this._changes = observableValue(this, []); this.changes = this._changes; } @@ -462,7 +462,7 @@ export class RemoteNewSession extends Disposable implements ICopilotChatSession private readonly _workspaceData = observableValue(this, undefined); readonly workspace: IObservable = this._workspaceData; - readonly changes: IObservable = observableValue(this, []); + readonly changes: IObservable = observableValue(this, []); private readonly _modelIdObservable = observableValue(this, undefined); readonly modelId: IObservable = this._modelIdObservable; @@ -704,7 +704,7 @@ class ClaudeCodeNewSession extends Disposable implements ICopilotChatSession { private readonly _workspaceData = observableValue(this, undefined); readonly workspace: IObservable = this._workspaceData; - readonly changes: IObservable = observableValue(this, []); + readonly changes: IObservable = observableValue(this, []); private readonly _modelIdObservable = observableValue(this, undefined); readonly modelId: IObservable = this._modelIdObservable; @@ -843,8 +843,8 @@ class AgentSessionAdapter implements ICopilotChatSession { private readonly _status: ReturnType>; readonly status: IObservable; - private readonly _changes: ReturnType>; - readonly changes: IObservable; + private readonly _changes: ReturnType>; + readonly changes: IObservable; readonly modelId: IObservable; readonly mode: IObservable<{ readonly id: string; readonly kind: string } | undefined>; @@ -895,7 +895,7 @@ class AgentSessionAdapter implements ICopilotChatSession { this._status = observableValue(this, toSessionStatus(session.status)); this.status = this._status; - this._changes = observableValue(this, this._extractChanges(session)); + this._changes = observableValue(this, this._extractChanges(session)); this.changes = this._changes; this.modelId = observableValue(this, undefined); @@ -1093,12 +1093,12 @@ class AgentSessionAdapter implements ICopilotChatSession { return undefined; } - private _extractChanges(session: IAgentSession): readonly IChatSessionFileChange[] { + private _extractChanges(session: IAgentSession): readonly ISessionFileChange[] { if (!session.changes) { return []; } if (Array.isArray(session.changes)) { - return session.changes as IChatSessionFileChange[]; + return session.changes as ISessionFileChange[]; } // Summary object — create a synthetic entry for total insertions/deletions const summary = session.changes as { readonly files: number; readonly insertions: number; readonly deletions: number }; diff --git a/src/vs/sessions/contrib/editor/browser/editor.contribution.ts b/src/vs/sessions/contrib/editor/browser/editor.contribution.ts index f8f9928377b6c..0d6e2ab4bd97f 100644 --- a/src/vs/sessions/contrib/editor/browser/editor.contribution.ts +++ b/src/vs/sessions/contrib/editor/browser/editor.contribution.ts @@ -9,9 +9,18 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { EditorPartModalContext, IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; import { IAgentWorkbenchLayoutService } from '../../../browser/workbench.js'; import { EditorMaximizedContext } from '../../../common/contextkeys.js'; +import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; +import { MultiDiffEditorInput } from '../../../../workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.js'; +import { CHANGES_VIEW_ID } from '../../changes/common/changes.js'; +import { ChangesViewPane } from '../../changes/browser/changesView.js'; +import { prepareMoveCopyEditors } from '../../../../workbench/browser/parts/editor/editor.js'; +import { Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; +import { MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID } from '../../../../workbench/browser/parts/editor/editorCommands.js'; class MaximizeMainEditorPartAction extends Action2 { static readonly ID = 'workbench.action.agentSessions.maximizeMainEditorPart'; @@ -25,7 +34,7 @@ class MaximizeMainEditorPartAction extends Action2 { menu: { id: MenuId.EditorTitleLayout, group: 'navigation', - order: 1, + order: 99, when: ContextKeyExpr.and( IsSessionsWindowContext, EditorMaximizedContext.negate()) @@ -53,7 +62,7 @@ class RestoreMainEditorPartAction extends Action2 { menu: { id: MenuId.EditorTitleLayout, group: 'navigation', - order: 1, + order: 99, when: ContextKeyExpr.and( IsSessionsWindowContext, EditorMaximizedContext) @@ -94,3 +103,117 @@ class CloseMainEditorPartAction extends Action2 { } registerAction2(CloseMainEditorPartAction); + +class OpenEditorInModalEditorAction extends Action2 { + static readonly ID = 'workbench.action.agentSessions.openEditorInModal'; + + constructor() { + super({ + id: OpenEditorInModalEditorAction.ID, + title: localize2('openEditorInModal', "Open in Modal Editor"), + icon: Codicon.openInWindow, + f1: false, + menu: { + id: MenuId.EditorTitleLayout, + group: 'navigation', + order: 1, + when: IsSessionsWindowContext + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const configurationService = accessor.get(IConfigurationService); + const editorGroupsService = accessor.get(IEditorGroupsService); + + // Set the `workbench.editor.useModal` setting to 'all' + await configurationService.updateValue('workbench.editor.useModal', 'all'); + + // Move all editors from the active group to the modal editor + const activeGroup = editorGroupsService.mainPart.activeGroup; + + // Check for multi-file diff editor + const multiFileDiffEditor = activeGroup.editors + .find(editor => editor instanceof MultiDiffEditorInput); + + if (multiFileDiffEditor) { + // Reopen multi-file diff editor as the first editor in the modal editor + const view = viewsService.getViewWithId(CHANGES_VIEW_ID); + await view?.openChanges(); + + // Close the multi-file diff editor + await activeGroup.closeEditor(multiFileDiffEditor); + } + + // Move all remaining editors to the modal editor + const modalPart = await editorGroupsService.createModalEditorPart(); + const editorsToMove = prepareMoveCopyEditors(activeGroup, activeGroup.editors.slice(), true); + activeGroup.moveEditors(editorsToMove, modalPart.activeGroup); + modalPart.activeGroup.focus(); + } +} + +registerAction2(OpenEditorInModalEditorAction); + +class OpenModalEditorInEditorAction extends Action2 { + static readonly ID = 'workbench.action.agentSessions.openModalEditorInEditor'; + + constructor() { + super({ + id: OpenModalEditorInEditorAction.ID, + title: localize2('openModalEditorInEditor', "Open in Editor"), + icon: Codicon.openInWindow, + f1: false, + menu: { + id: MenuId.ModalEditorTitle, + group: 'navigation', + order: 98, + when: ContextKeyExpr.and( + IsSessionsWindowContext, + EditorPartModalContext) + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const commandService = accessor.get(ICommandService); + const configurationService = accessor.get(IConfigurationService); + const editorGroupsService = accessor.get(IEditorGroupsService); + const layoutService = accessor.get(IAgentWorkbenchLayoutService); + + const activeGroup = editorGroupsService.activeModalEditorPart?.activeGroup; + if (!activeGroup) { + return; + } + + // Set the `workbench.editor.useModal` setting back to 'some' + await configurationService.updateValue('workbench.editor.useModal', 'some'); + + // Show the main editor part + layoutService.setPartHidden(false, Parts.EDITOR_PART); + + // Check for navigation in the modal editor + const navigation = activeGroup.activeEditorPane?.options?.modal?.navigation; + if (navigation) { + const view = viewsService.getViewWithId(CHANGES_VIEW_ID); + const changes = view?.viewModel.activeSessionChangesObs.get(); + + if (changes && navigation.current < changes.length) { + // Reopen multi-file diff editor for the current file + await view?.openChanges(changes[navigation.current].modifiedUri ?? changes[navigation.current].originalUri); + + // Close the editor in the modal editor (assume that the + // multi-file diff editor is the first editor in the modal + // editor) + await activeGroup.closeEditor(activeGroup.editors[0]); + } + } + + // Move all remaining editors to the main editor part + await commandService.executeCommand(MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID); + } +} + +registerAction2(OpenModalEditorInEditorAction); diff --git a/src/vs/sessions/services/sessions/common/session.ts b/src/vs/sessions/services/sessions/common/session.ts index 8d97f27a7506c..c7627051c57c9 100644 --- a/src/vs/sessions/services/sessions/common/session.ts +++ b/src/vs/sessions/services/sessions/common/session.ts @@ -9,7 +9,7 @@ import { IObservable } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; -import { IChatSessionFileChange } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { IChatSessionFileChange, IChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; export interface ISessionType { /** Unique identifier (e.g., 'copilot-cli', 'copilot-cloud', 'claude-code'). */ @@ -117,6 +117,8 @@ export interface IGitHubInfo { }; } +export type ISessionFileChange = IChatSessionFileChange | IChatSessionFileChange2; + /** * A single chat within a session, produced by the sessions management layer. */ @@ -135,7 +137,7 @@ export interface IChat { /** Current chat status. */ readonly status: IObservable; /** File changes produced by the chat. */ - readonly changes: IObservable; + readonly changes: IObservable; /** Currently selected model identifier. */ readonly modelId: IObservable; /** Currently selected mode identifier and kind. */ @@ -179,7 +181,7 @@ export interface ISession { /** Current session status. */ readonly status: IObservable; /** File changes produced by the session. */ - readonly changes: IObservable; + readonly changes: IObservable; /** Currently selected model identifier. */ readonly modelId: IObservable; /** Currently selected mode identifier and kind. */ diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index fa93ea472e090..d4a45aeae6795 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -264,13 +264,8 @@ import '../workbench/contrib/git/browser/git.contributions.js'; // SCM import '../workbench/contrib/scm/browser/scm.contribution.js'; -// Debug -import '../workbench/contrib/debug/browser/debug.contribution.js'; -import '../workbench/contrib/debug/browser/debugEditorContribution.js'; -import '../workbench/contrib/debug/browser/breakpointEditorContribution.js'; -import '../workbench/contrib/debug/browser/callStackEditorContribution.js'; -import '../workbench/contrib/debug/browser/repl.js'; -import '../workbench/contrib/debug/browser/debugViewlet.js'; +// Debug (service) +import '../workbench/contrib/debug/browser/debug.service.contribution.js'; // Markers import '../workbench/contrib/markers/browser/markers.contribution.js'; diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index ef66fad7e3bc7..7c8fdbf44beaa 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -1515,7 +1515,7 @@ function registerModalEditorCommands(): void { menu: { id: MenuId.ModalEditorTitle, group: 'navigation', - order: 1 + order: 99 } }); } @@ -1552,7 +1552,7 @@ function registerModalEditorCommands(): void { menu: { id: MenuId.ModalEditorTitle, group: 'navigation', - order: 2 + order: 100 } }); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 87d1743b42f76..2fa8262672846 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -1007,7 +1007,7 @@ export class AICustomizationManagementEditor extends EditorPane { /** * Navigates to the welcome page (no section selected). */ - private showWelcomePage(): void { + public showWelcomePage(): void { if (this.viewMode === 'editor') { this.goBackToList(); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 2774b967bfa67..f01e22e33658d 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -168,6 +168,9 @@ import { IPluginGitService } from '../common/plugins/pluginGitService.js'; import { PluginInstallService } from './pluginInstallService.js'; import './promptSyntax/promptCodingAgentActionContribution.js'; import './promptSyntax/promptToolsCodeLensProvider.js'; +import './planReviewFeedback/planReviewFeedbackEditorContribution.js'; +import { registerPlanReviewFeedbackEditorActions } from './planReviewFeedback/planReviewFeedbackEditorActions.js'; +import { IPlanReviewFeedbackService, PlanReviewFeedbackService } from './planReviewFeedback/planReviewFeedbackService.js'; import { ChatSlashCommandsContribution } from './chatSlashCommands.js'; import { PluginUrlHandler } from './pluginUrlHandler.js'; import { PromptUrlHandler } from './promptSyntax/promptUrlHandler.js'; @@ -2164,6 +2167,7 @@ registerChatElicitationActions(); registerChatToolActions(); registerLanguageModelActions(); registerChatPluginActions(); +registerPlanReviewFeedbackEditorActions(); registerAction2(ConfigureToolSets); registerEditorFeature(ChatPasteProvidersFeature); @@ -2209,6 +2213,7 @@ registerSingleton(IChatTodoListService, ChatTodoListService, InstantiationType.D registerSingleton(IChatArtifactsService, ChatArtifactsService, InstantiationType.Delayed); registerSingleton(IChatOutputRendererService, ChatOutputRendererService, InstantiationType.Delayed); registerSingleton(IChatLayoutService, ChatLayoutService, InstantiationType.Delayed); +registerSingleton(IPlanReviewFeedbackService, PlanReviewFeedbackService, InstantiationType.Delayed); registerSingleton(IChatTipService, ChatTipService, InstantiationType.Delayed); registerSingleton(IChatDebugService, ChatDebugServiceImpl, InstantiationType.Delayed); registerSingleton(IChatImageCarouselService, ChatImageCarouselService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/planReviewFeedback/media/planReviewFeedback.css b/src/vs/workbench/contrib/chat/browser/planReviewFeedback/media/planReviewFeedback.css new file mode 100644 index 0000000000000..a00c20cdca301 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/planReviewFeedback/media/planReviewFeedback.css @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.plan-review-feedback-input-widget { + position: absolute; + z-index: 10000; + background-color: var(--vscode-panel-background); + border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); + box-shadow: var(--vscode-shadow-lg); + border-radius: 8px; + padding: 4px; + display: flex; + flex-direction: row; + align-items: flex-end; + animation: planReviewFeedbackInputAppear 0.15s ease-out; +} + +@keyframes planReviewFeedbackInputAppear { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .plan-review-feedback-input-widget { + animation: none; + transform: none; + } +} + +.plan-review-feedback-input-widget textarea { + background-color: var(--vscode-panel-background); + border: none; + color: var(--vscode-input-foreground); + font: inherit; + border-radius: 4px; + padding: 0 0 0 6px; + outline: none; + min-width: 150px; + max-width: 400px; + resize: none; + overflow-y: hidden; + white-space: pre-wrap; + word-wrap: break-word; + box-sizing: border-box; + display: block; + flex: 1; +} + +.plan-review-feedback-input-widget textarea:focus { + border-color: var(--vscode-focusBorder); + outline: none !important; +} + +.plan-review-feedback-input-widget textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} + +.plan-review-feedback-input-widget .plan-review-feedback-input-measure { + position: absolute; + visibility: hidden; + height: 0; + overflow: hidden; + white-space: pre; + font: inherit; + font-size: 13px; +} + +.plan-review-feedback-input-widget .plan-review-feedback-input-actions { + display: flex; + align-items: center; + margin-left: 2px; + flex-shrink: 0; +} + +.plan-review-feedback-input-widget .plan-review-feedback-input-actions .action-bar .action-item .action-label { + width: 16px; + height: 16px; +} + +/* Overlay toolbar (submit / navigate / clear actions) */ +.plan-review-feedback-overlay-widget { + padding: 2px 4px; + color: var(--vscode-foreground); + background-color: var(--vscode-editorWidget-background); + border-radius: 6px; + border: 1px solid var(--vscode-editorHoverWidget-border); + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + z-index: 10; + box-shadow: var(--vscode-shadow-lg); + overflow: hidden; +} + +.plan-review-feedback-overlay-widget .action-item > .action-label { + padding: 4px 6px; + font-size: 11px; + line-height: 14px; + border-radius: 4px; +} + +.plan-review-feedback-overlay-widget .monaco-action-bar .actions-container { + gap: 4px; +} + +.plan-review-feedback-overlay-widget .action-item.primary > .action-label { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.monaco-workbench .plan-review-feedback-overlay-widget .monaco-action-bar .action-item.primary > .action-label:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.plan-review-feedback-overlay-widget .action-item > .action-label.codicon:not(.separator) { + color: var(--vscode-foreground); + width: 22px; + height: 22px; + padding: 0; + font-size: 16px; + line-height: 22px; + display: flex; + align-items: center; + justify-content: center; +} + +.plan-review-feedback-overlay-widget .monaco-action-bar .action-item.disabled { + + > .action-label.codicon::before, + > .action-label.codicon, + > .action-label, + > .action-label:hover { + color: var(--vscode-button-separator); + opacity: 1; + } +} + +.plan-review-feedback-overlay-widget .action-item.label-item { + font-variant-numeric: tabular-nums; +} + +.plan-review-feedback-overlay-widget .monaco-action-bar .action-item.label-item > .action-label, +.plan-review-feedback-overlay-widget .monaco-action-bar .action-item.label-item > .action-label:hover { + color: var(--vscode-foreground); + opacity: 1; +} + +/* Gutter decoration for feedback lines */ +.plan-review-feedback-line-number { + color: var(--vscode-editorInfo-foreground); +} diff --git a/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackEditorActions.ts b/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackEditorActions.ts new file mode 100644 index 0000000000000..e10a1da941364 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackEditorActions.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../../base/common/codicons.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { IPlanReviewFeedbackService } from './planReviewFeedbackService.js'; +import { CHAT_CATEGORY } from '../actions/chatActions.js'; + +export const PlanReviewFeedbackMenuId = MenuId.for('planReviewFeedback.editorContent'); + +export const hasPlanReviewFeedback = new RawContextKey('planReviewFeedback.hasFeedback', false); + +export const submitPlanReviewFeedbackActionId = 'planReviewFeedback.action.submit'; +export const navigatePreviousPlanReviewFeedbackActionId = 'planReviewFeedback.action.navigatePrevious'; +export const navigateNextPlanReviewFeedbackActionId = 'planReviewFeedback.action.navigateNext'; +export const clearAllPlanReviewFeedbackActionId = 'planReviewFeedback.action.clearAll'; +export const navigationBearingFakeActionId = 'planReviewFeedback.navigation.bearings'; + +function getActivePlanUri(accessor: ServicesAccessor): URI | undefined { + const editorService = accessor.get(IEditorService); + const planReviewFeedbackService = accessor.get(IPlanReviewFeedbackService); + + const activeEditor = editorService.activeEditor; + if (!activeEditor) { + return undefined; + } + + const resource = activeEditor.resource; + if (!resource) { + return undefined; + } + + if (planReviewFeedbackService.isActivePlanReview(resource)) { + return resource; + } + + return undefined; +} + +class SubmitPlanReviewFeedbackAction extends Action2 { + + constructor() { + super({ + id: submitPlanReviewFeedbackActionId, + title: localize2('planReviewFeedback.submit', 'Submit Feedback'), + shortTitle: localize2('planReviewFeedback.submitShort', 'Submit'), + icon: Codicon.send, + category: CHAT_CATEGORY, + precondition: hasPlanReviewFeedback, + menu: { + id: PlanReviewFeedbackMenuId, + group: 'a_submit', + order: 0, + when: hasPlanReviewFeedback, + }, + }); + } + + override run(accessor: ServicesAccessor): void { + const planUri = getActivePlanUri(accessor); + if (!planUri) { + return; + } + + const planReviewFeedbackService = accessor.get(IPlanReviewFeedbackService); + planReviewFeedbackService.submitAllFeedback(planUri); + } +} + +class NavigatePlanReviewFeedbackAction extends Action2 { + + constructor(private readonly _next: boolean) { + super({ + id: _next ? navigateNextPlanReviewFeedbackActionId : navigatePreviousPlanReviewFeedbackActionId, + title: _next + ? localize2('planReviewFeedback.next', 'Go to Next Feedback Comment') + : localize2('planReviewFeedback.previous', 'Go to Previous Feedback Comment'), + icon: _next ? Codicon.arrowDown : Codicon.arrowUp, + category: CHAT_CATEGORY, + f1: true, + precondition: hasPlanReviewFeedback, + menu: { + id: PlanReviewFeedbackMenuId, + group: 'navigate', + order: _next ? 2 : 1, + when: hasPlanReviewFeedback, + }, + }); + } + + override run(accessor: ServicesAccessor): void { + const planUri = getActivePlanUri(accessor); + if (!planUri) { + return; + } + + const planReviewFeedbackService = accessor.get(IPlanReviewFeedbackService); + const editorService = accessor.get(IEditorService); + + const item = planReviewFeedbackService.getNextFeedback(planUri, this._next); + if (!item) { + return; + } + + // Reveal the feedback item in the editor + const editor = editorService.activeTextEditorControl; + if (editor && isCodeEditor(editor)) { + editor.revealLineInCenter(item.line); + editor.setPosition({ lineNumber: item.line, column: item.column }); + } + } +} + +class ClearAllPlanReviewFeedbackAction extends Action2 { + + constructor() { + super({ + id: clearAllPlanReviewFeedbackActionId, + title: localize2('planReviewFeedback.clear', 'Clear'), + tooltip: localize2('planReviewFeedback.clearAllTooltip', 'Clear All Feedback'), + icon: Codicon.clearAll, + category: CHAT_CATEGORY, + f1: true, + precondition: hasPlanReviewFeedback, + menu: { + id: PlanReviewFeedbackMenuId, + group: 'a_submit', + order: 1, + when: hasPlanReviewFeedback, + }, + }); + } + + override run(accessor: ServicesAccessor): void { + const planUri = getActivePlanUri(accessor); + if (!planUri) { + return; + } + + const planReviewFeedbackService = accessor.get(IPlanReviewFeedbackService); + planReviewFeedbackService.clearFeedback(planUri); + } +} + +export function registerPlanReviewFeedbackEditorActions(): void { + registerAction2(SubmitPlanReviewFeedbackAction); + registerAction2(class extends NavigatePlanReviewFeedbackAction { constructor() { super(false); } }); + registerAction2(class extends NavigatePlanReviewFeedbackAction { constructor() { super(true); } }); + registerAction2(ClearAllPlanReviewFeedbackAction); + + MenuRegistry.appendMenuItem(PlanReviewFeedbackMenuId, { + command: { + id: navigationBearingFakeActionId, + title: localize('label', 'Navigation Status'), + precondition: ContextKeyExpr.false(), + }, + group: 'navigate', + order: -1, + when: hasPlanReviewFeedback, + }); +} diff --git a/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackEditorContribution.ts b/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackEditorContribution.ts new file mode 100644 index 0000000000000..a1e651e4a23a5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackEditorContribution.ts @@ -0,0 +1,723 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/planReviewFeedback.css'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../../editor/browser/editorBrowser.js'; +import { IEditorContribution, IEditorDecorationsCollection } from '../../../../../editor/common/editorCommon.js'; +import { EditorContributionInstantiation, registerEditorContribution } from '../../../../../editor/browser/editorExtensions.js'; +import { EditorOption } from '../../../../../editor/common/config/editorOptions.js'; +import { SelectionDirection } from '../../../../../editor/common/core/selection.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { OverviewRulerLane } from '../../../../../editor/common/model.js'; +import { themeColorFromId } from '../../../../../platform/theme/common/themeService.js'; +import { overviewRulerInfo } from '../../../../../editor/common/core/editorColorRegistry.js'; +import { addStandardDisposableListener, getWindow, ModifierKeyEmitter } from '../../../../../base/browser/dom.js'; +import { KeyCode } from '../../../../../base/common/keyCodes.js'; +import { localize } from '../../../../../nls.js'; +import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js'; +import { Action } from '../../../../../base/common/actions.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; +import { IPlanReviewFeedbackService } from './planReviewFeedbackService.js'; +import { hasPlanReviewFeedback, navigationBearingFakeActionId, PlanReviewFeedbackMenuId, submitPlanReviewFeedbackActionId } from './planReviewFeedbackEditorActions.js'; +import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { autorun, observableValue } from '../../../../../base/common/observable.js'; + +class PlanReviewFeedbackInputWidget implements IOverlayWidget { + + private static readonly _ID = 'planReviewFeedback.inputWidget'; + private static readonly _MIN_WIDTH = 150; + private static readonly _MAX_WIDTH = 400; + + readonly allowEditorOverflow = false; + + private readonly _domNode: HTMLElement; + private readonly _inputElement: HTMLTextAreaElement; + private readonly _measureElement: HTMLElement; + private readonly _actionBar: ActionBar; + private readonly _addAction: Action; + private readonly _addAndSubmitAction: Action; + private _position: IOverlayWidgetPosition | null = null; + private _lineHeight = 22; + + private readonly _onDidTriggerAdd = new Emitter(); + readonly onDidTriggerAdd: Event = this._onDidTriggerAdd.event; + + private readonly _onDidTriggerAddAndSubmit = new Emitter(); + readonly onDidTriggerAddAndSubmit: Event = this._onDidTriggerAddAndSubmit.event; + + constructor( + private readonly _editor: ICodeEditor, + ) { + this._domNode = document.createElement('div'); + this._domNode.classList.add('plan-review-feedback-input-widget'); + this._domNode.style.display = 'none'; + + this._inputElement = document.createElement('textarea'); + this._inputElement.rows = 1; + this._inputElement.placeholder = localize('planReviewFeedback.addFeedback', "Add Feedback"); + this._domNode.appendChild(this._inputElement); + + // Hidden element used to measure text width for auto-growing + this._measureElement = document.createElement('span'); + this._measureElement.classList.add('plan-review-feedback-input-measure'); + this._domNode.appendChild(this._measureElement); + + // Action bar with add/submit actions + const actionsContainer = document.createElement('div'); + actionsContainer.classList.add('plan-review-feedback-input-actions'); + this._domNode.appendChild(actionsContainer); + + this._addAction = new Action( + 'planReviewFeedback.add', + localize('planReviewFeedback.add', "Add Feedback (Enter)"), + ThemeIcon.asClassName(Codicon.plus), + false, + () => { this._onDidTriggerAdd.fire(); return Promise.resolve(); } + ); + + this._addAndSubmitAction = new Action( + 'planReviewFeedback.addAndSubmit', + localize('planReviewFeedback.addAndSubmit', "Add Feedback and Submit (Alt+Enter)"), + ThemeIcon.asClassName(Codicon.send), + false, + () => { this._onDidTriggerAddAndSubmit.fire(); return Promise.resolve(); } + ); + + this._actionBar = new ActionBar(actionsContainer); + this._actionBar.push(this._addAction, { icon: true, label: false, keybinding: localize('enter', "Enter") }); + + // Toggle to alt action when Alt key is held + const modifierKeyEmitter = ModifierKeyEmitter.getInstance(); + modifierKeyEmitter.event(status => { + this._updateActionForAlt(status.altKey); + }); + + this._inputElement.style.lineHeight = `${this._lineHeight}px`; + } + + private _isShowingAlt = false; + + private _updateActionForAlt(altKey: boolean): void { + if (altKey && !this._isShowingAlt) { + this._isShowingAlt = true; + this._actionBar.clear(); + this._actionBar.push(this._addAndSubmitAction, { icon: true, label: false, keybinding: localize('altEnter', "Alt+Enter") }); + } else if (!altKey && this._isShowingAlt) { + this._isShowingAlt = false; + this._actionBar.clear(); + this._actionBar.push(this._addAction, { icon: true, label: false, keybinding: localize('enter', "Enter") }); + } + } + + getId(): string { + return PlanReviewFeedbackInputWidget._ID; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IOverlayWidgetPosition | null { + return this._position; + } + + get inputElement(): HTMLTextAreaElement { + return this._inputElement; + } + + setPosition(position: IOverlayWidgetPosition | null): void { + this._position = position; + this._editor.layoutOverlayWidget(this); + } + + show(): void { + this._domNode.style.display = ''; + } + + hide(): void { + this._domNode.style.display = 'none'; + } + + clearInput(): void { + this._inputElement.value = ''; + this._updateActionEnabled(); + this._autoSize(); + } + + autoSize(): void { + this._autoSize(); + } + + updateActionEnabled(): void { + this._updateActionEnabled(); + } + + private _updateActionEnabled(): void { + const hasText = this._inputElement.value.trim().length > 0; + this._addAction.enabled = hasText; + this._addAndSubmitAction.enabled = hasText; + } + + private _autoSize(): void { + const text = this._inputElement.value || this._inputElement.placeholder; + + // Measure the text width using the hidden span + this._measureElement.textContent = text; + const textWidth = this._measureElement.scrollWidth; + + // Clamp width between min and max + const width = Math.max(PlanReviewFeedbackInputWidget._MIN_WIDTH, Math.min(textWidth + 10, PlanReviewFeedbackInputWidget._MAX_WIDTH)); + this._inputElement.style.width = `${width}px`; + + // Reset height to auto then expand to fit all content, with a minimum of 1 line + this._inputElement.style.height = 'auto'; + const newHeight = Math.max(this._inputElement.scrollHeight, this._lineHeight); + this._inputElement.style.height = `${newHeight}px`; + } + + dispose(): void { + this._actionBar.dispose(); + this._addAction.dispose(); + this._addAndSubmitAction.dispose(); + this._onDidTriggerAdd.dispose(); + this._onDidTriggerAddAndSubmit.dispose(); + } +} + +class PlanReviewFeedbackOverlayWidget implements IOverlayWidget { + + private static readonly _ID = 'planReviewFeedback.overlayWidget'; + + private readonly _domNode: HTMLElement; + private readonly _toolbarNode: HTMLElement; + private readonly _showStore = new DisposableStore(); + private readonly _navigationBearings = observableValue<{ activeIdx: number; totalCount: number }>('planReviewFeedbackBearings', { activeIdx: -1, totalCount: 0 }); + + constructor( + _codeEditor: ICodeEditor, + private readonly _instaService: IInstantiationService, + ) { + this._domNode = document.createElement('div'); + this._domNode.classList.add('plan-review-feedback-overlay-widget'); + this._domNode.style.display = 'none'; + + this._toolbarNode = document.createElement('div'); + this._toolbarNode.classList.add('plan-review-feedback-overlay-toolbar'); + this._domNode.appendChild(this._toolbarNode); + } + + getId(): string { + return PlanReviewFeedbackOverlayWidget._ID; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IOverlayWidgetPosition | null { + return { preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER }; + } + + show(navigationBearings: { activeIdx: number; totalCount: number }): void { + this._showStore.clear(); + this._navigationBearings.set(navigationBearings, undefined); + this._domNode.style.display = ''; + + this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, PlanReviewFeedbackMenuId, { + telemetrySource: 'planReviewFeedback.overlayToolbar', + hiddenItemStrategy: HiddenItemStrategy.Ignore, + toolbarOptions: { + primaryGroup: () => true, + useSeparatorsInPrimaryActions: true, + }, + menuOptions: { renderShortTitle: true }, + actionViewItemProvider: (action, options) => { + if (action.id === navigationBearingFakeActionId) { + const that = this; + return new class extends ActionViewItem { + constructor() { + super(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true }); + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('label-item'); + + this._store.add(autorun(r => { + if (!this.label) { + return; + } + const { activeIdx, totalCount } = that._navigationBearings.read(r); + if (totalCount > 0) { + const current = activeIdx === -1 ? 1 : activeIdx + 1; + this.label.innerText = localize('nOfM', '{0}/{1}', current, totalCount); + } else { + this.label.innerText = localize('zero', '0/0'); + } + })); + } + }; + } + + const isPrimary = action.id === submitPlanReviewFeedbackActionId; + return new class extends ActionViewItem { + constructor() { + super(undefined, action, { ...options, icon: !isPrimary, label: isPrimary, keybindingNotRenderedWithLabel: true }); + } + + override render(container: HTMLElement): void { + super.render(container); + if (isPrimary) { + this.element?.classList.add('primary'); + } + } + }; + }, + })); + } + + hide(): void { + this._showStore.clear(); + this._domNode.style.display = 'none'; + this._navigationBearings.set({ activeIdx: -1, totalCount: 0 }, undefined); + } + + dispose(): void { + this._showStore.dispose(); + } +} + +export class PlanReviewFeedbackEditorContribution extends Disposable implements IEditorContribution { + + static readonly ID = 'planReviewFeedback.editorContribution'; + + private _widget: PlanReviewFeedbackInputWidget | undefined; + private _overlayWidget: PlanReviewFeedbackOverlayWidget | undefined; + private _visible = false; + private _mouseDown = false; + private _suppressSelectionChangeOnce = false; + private _isActivePlan = false; + private readonly _widgetListeners = this._register(new DisposableStore()); + private readonly _decorations: IEditorDecorationsCollection; + private readonly _hasFeedbackContextKey; + + constructor( + private readonly _editor: ICodeEditor, + @IPlanReviewFeedbackService private readonly _planReviewFeedbackService: IPlanReviewFeedbackService, + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._decorations = this._editor.createDecorationsCollection(); + this._hasFeedbackContextKey = hasPlanReviewFeedback.bindTo(contextKeyService); + + this._register(this._editor.onDidChangeCursorSelection(() => this._onSelectionChanged())); + this._register(this._editor.onDidChangeModel(() => this._onModelChanged())); + this._register(this._editor.onDidScrollChange(() => { + if (this._visible) { + this._updatePosition(); + } + })); + this._register(this._editor.onMouseDown((e) => { + if (this._isWidgetTarget(e.event.target)) { + return; + } + this._mouseDown = true; + this._hide(); + })); + this._register(this._editor.onMouseUp((e) => { + this._mouseDown = false; + if (this._isWidgetTarget(e.event.target)) { + return; + } + this._onSelectionChanged(); + })); + this._register(this._editor.onDidBlurEditorWidget(() => { + if (!this._visible) { + return; + } + getWindow(this._editor.getDomNode()!).setTimeout(() => { + if (!this._visible) { + return; + } + if (this._isWidgetTarget(getWindow(this._editor.getDomNode()!).document.activeElement)) { + return; + } + this._hide(); + }, 0); + })); + this._register(this._editor.onDidFocusEditorText(() => this._onSelectionChanged())); + this._register(this._planReviewFeedbackService.onDidChangeRegistrations(() => this._onModelChanged())); + this._register(this._planReviewFeedbackService.onDidChangeFeedback(() => this._updateDecorations())); + this._register(this._planReviewFeedbackService.onDidChangeNavigation(() => this._updateDecorations())); + } + + private _isWidgetTarget(target: EventTarget | Element | null): boolean { + return !!this._widget && !!target && this._widget.getDomNode().contains(target as Node); + } + + private _ensureWidget(): PlanReviewFeedbackInputWidget { + if (!this._widget) { + this._widget = new PlanReviewFeedbackInputWidget(this._editor); + this._register(this._widget.onDidTriggerAdd(() => this._addFeedback())); + this._register(this._widget.onDidTriggerAddAndSubmit(() => this._addFeedbackAndSubmit())); + this._editor.addOverlayWidget(this._widget); + } + return this._widget; + } + + private _onModelChanged(): void { + this._hide(); + this._suppressSelectionChangeOnce = false; + + const model = this._editor.getModel(); + this._isActivePlan = !!model && this._planReviewFeedbackService.isActivePlanReview(model.uri); + this._updateDecorations(); + } + + private _onSelectionChanged(): void { + if (this._suppressSelectionChangeOnce) { + this._suppressSelectionChangeOnce = false; + return; + } + + if (this._mouseDown || !this._editor.hasTextFocus()) { + return; + } + + if (!this._isActivePlan) { + this._hide(); + return; + } + + const selection = this._editor.getSelection(); + if (!selection) { + this._hide(); + return; + } + + const model = this._editor.getModel(); + if (!model) { + this._hide(); + return; + } + + this._show(); + } + + private _show(): void { + const widget = this._ensureWidget(); + + if (!this._visible) { + this._visible = true; + this._registerWidgetListeners(widget); + } + + widget.clearInput(); + widget.show(); + this._updatePosition(); + } + + private _hide(): void { + if (!this._visible) { + return; + } + + this._visible = false; + this._widgetListeners.clear(); + + if (this._widget) { + this._widget.hide(); + this._widget.setPosition(null); + this._widget.clearInput(); + } + } + + private _registerWidgetListeners(widget: PlanReviewFeedbackInputWidget): void { + this._widgetListeners.clear(); + + const editorDomNode = this._editor.getDomNode(); + if (editorDomNode) { + this._widgetListeners.add(addStandardDisposableListener(editorDomNode, 'keydown', e => { + if (!this._visible) { + return; + } + + if (!this._editor.hasTextFocus()) { + return; + } + + if (e.keyCode === KeyCode.Ctrl || e.keyCode === KeyCode.Shift || e.keyCode === KeyCode.Alt || e.keyCode === KeyCode.Meta) { + return; + } + + if (e.keyCode === KeyCode.Escape) { + this._hide(); + this._editor.focus(); + return; + } + + // Ctrl+I / Cmd+I explicitly focuses the feedback input + if ((e.ctrlKey || e.metaKey) && e.keyCode === KeyCode.KeyI) { + e.preventDefault(); + e.stopPropagation(); + widget.inputElement.focus(); + return; + } + + if (e.ctrlKey || e.altKey || e.metaKey) { + return; + } + + // Keep caret/navigation keys in the editor + if ( + e.keyCode === KeyCode.UpArrow + || e.keyCode === KeyCode.DownArrow + || e.keyCode === KeyCode.LeftArrow + || e.keyCode === KeyCode.RightArrow + ) { + return; + } + + // Only auto-focus the input on typing when the document is readonly + if (!this._editor.getOption(EditorOption.readOnly)) { + return; + } + + if (getWindow(widget.inputElement).document.activeElement !== widget.inputElement) { + widget.inputElement.focus(); + } + })); + } + + // Listen for keydown on the input element + this._widgetListeners.add(addStandardDisposableListener(widget.inputElement, 'keydown', e => { + if (e.keyCode === KeyCode.Escape) { + e.preventDefault(); + e.stopPropagation(); + this._hide(); + this._editor.focus(); + return; + } + + if (e.keyCode === KeyCode.Enter && e.altKey) { + e.preventDefault(); + e.stopPropagation(); + this._addFeedbackAndSubmit(); + return; + } + + if (e.keyCode === KeyCode.Enter) { + e.preventDefault(); + e.stopPropagation(); + this._addFeedback(); + return; + } + })); + + // Stop propagation of input events so the editor doesn't handle them + this._widgetListeners.add(addStandardDisposableListener(widget.inputElement, 'keypress', e => { + e.stopPropagation(); + })); + + // Auto-size the textarea as the user types + this._widgetListeners.add(addStandardDisposableListener(widget.inputElement, 'input', () => { + widget.autoSize(); + widget.updateActionEnabled(); + this._updatePosition(); + })); + + // Hide when input loses focus to something outside both editor and widget + this._widgetListeners.add(addStandardDisposableListener(widget.inputElement, 'blur', () => { + const win = getWindow(widget.inputElement); + win.setTimeout(() => { + if (!this._visible) { + return; + } + if (this._editor.hasWidgetFocus()) { + return; + } + this._hide(); + }, 0); + })); + } + + private _hideAndRefocusEditor(): void { + this._suppressSelectionChangeOnce = true; + this._hide(); + this._editor.focus(); + } + + private _addFeedback(): boolean { + if (!this._widget) { + return false; + } + + const text = this._widget.inputElement.value.trim(); + if (!text) { + return false; + } + + const selection = this._editor.getSelection(); + const model = this._editor.getModel(); + if (!selection || !model) { + return false; + } + + const line = selection.startLineNumber; + const column = selection.startColumn; + this._planReviewFeedbackService.addFeedback(model.uri, line, column, text); + this._hideAndRefocusEditor(); + return true; + } + + private _addFeedbackAndSubmit(): void { + if (!this._widget) { + return; + } + + const text = this._widget.inputElement.value.trim(); + if (!text) { + return; + } + + const selection = this._editor.getSelection(); + const model = this._editor.getModel(); + if (!selection || !model) { + return; + } + + const line = selection.startLineNumber; + const column = selection.startColumn; + this._planReviewFeedbackService.addFeedback(model.uri, line, column, text); + this._hideAndRefocusEditor(); + this._planReviewFeedbackService.submitAllFeedback(model.uri); + } + + private _updatePosition(): void { + if (!this._widget || !this._visible) { + return; + } + + const selection = this._editor.getSelection(); + if (!selection) { + this._hide(); + return; + } + + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const layoutInfo = this._editor.getLayoutInfo(); + const widgetDom = this._widget.getDomNode(); + const widgetHeight = widgetDom.offsetHeight || 30; + const widgetWidth = widgetDom.offsetWidth || 150; + + const cursorPosition = selection.getDirection() === SelectionDirection.LTR + ? selection.getEndPosition() + : selection.getStartPosition(); + + const scrolledPosition = this._editor.getScrolledVisiblePosition(cursorPosition); + if (!scrolledPosition) { + this._widget.setPosition(null); + return; + } + + // Place below for LTR, above for RTL, with flip if out of bounds + let top: number; + if (selection.getDirection() === SelectionDirection.LTR) { + top = scrolledPosition.top + lineHeight; + if (top + widgetHeight > layoutInfo.height) { + top = scrolledPosition.top - widgetHeight; + } + } else { + top = scrolledPosition.top - widgetHeight; + if (top < 0) { + top = scrolledPosition.top + lineHeight; + } + } + + top = Math.max(0, Math.min(top, layoutInfo.height - widgetHeight)); + const left = Math.max(0, Math.min(scrolledPosition.left, layoutInfo.width - widgetWidth)); + + this._widget.setPosition({ preference: { top, left } }); + } + + private _updateDecorations(): void { + const model = this._editor.getModel(); + if (!model || !this._isActivePlan) { + this._decorations.clear(); + this._hasFeedbackContextKey.set(false); + this._hideOverlayToolbar(); + return; + } + + const items = this._planReviewFeedbackService.getFeedback(model.uri); + this._hasFeedbackContextKey.set(items.length > 0); + + if (items.length > 0) { + const bearings = this._planReviewFeedbackService.getNavigationBearing(model.uri); + this._showOverlayToolbar(bearings); + } else { + this._hideOverlayToolbar(); + } + + this._decorations.set( + items.map(item => ({ + range: new Range(item.line, item.column, item.line, item.column), + options: { + description: 'plan-review-feedback', + glyphMarginClassName: ThemeIcon.asClassName(Codicon.comment), + glyphMarginHoverMessage: { value: item.text }, + overviewRuler: { + color: themeColorFromId(overviewRulerInfo), + position: OverviewRulerLane.Center, + }, + lineNumberClassName: 'plan-review-feedback-line-number', + } + })) + ); + } + + private _ensureOverlayWidget(): PlanReviewFeedbackOverlayWidget { + if (!this._overlayWidget) { + this._overlayWidget = new PlanReviewFeedbackOverlayWidget(this._editor, this._instantiationService); + this._editor.addOverlayWidget(this._overlayWidget); + } + return this._overlayWidget; + } + + private _showOverlayToolbar(bearings: { activeIdx: number; totalCount: number }): void { + const widget = this._ensureOverlayWidget(); + widget.show(bearings); + } + + private _hideOverlayToolbar(): void { + if (this._overlayWidget) { + this._overlayWidget.hide(); + } + } + + override dispose(): void { + if (this._widget) { + this._editor.removeOverlayWidget(this._widget); + this._widget.dispose(); + this._widget = undefined; + } + if (this._overlayWidget) { + this._editor.removeOverlayWidget(this._overlayWidget); + this._overlayWidget.dispose(); + this._overlayWidget = undefined; + } + super.dispose(); + } +} + +registerEditorContribution(PlanReviewFeedbackEditorContribution.ID, PlanReviewFeedbackEditorContribution, EditorContributionInstantiation.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackService.ts b/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackService.ts new file mode 100644 index 0000000000000..f6d8279ae98ae --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackService.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IChatPlanReviewResult } from '../../common/chatService/chatService.js'; + +export interface IPlanReviewFeedbackItem { + readonly id: string; + readonly line: number; + readonly column: number; + readonly text: string; +} + +export const IPlanReviewFeedbackService = createDecorator('planReviewFeedbackService'); + +export interface IPlanReviewFeedbackService { + readonly _serviceBrand: undefined; + + readonly onDidChangeFeedback: Event; + readonly onDidChangeNavigation: Event; + readonly onDidChangeRegistrations: Event; + + registerPlanReview(planUri: URI, onSubmit: (result: IChatPlanReviewResult) => void): IDisposable; + isActivePlanReview(uri: URI): boolean; + addFeedback(planUri: URI, line: number, column: number, text: string): string; + removeFeedback(planUri: URI, feedbackId: string): void; + updateFeedback(planUri: URI, feedbackId: string, newText: string): void; + getFeedback(planUri: URI): readonly IPlanReviewFeedbackItem[]; + clearFeedback(planUri: URI): void; + getNextFeedback(planUri: URI, next: boolean): IPlanReviewFeedbackItem | undefined; + getNavigationBearing(planUri: URI): { activeIdx: number; totalCount: number }; + setNavigationAnchor(planUri: URI, itemId: string | undefined): void; + submitAllFeedback(planUri: URI): void; +} + +interface IPlanReviewRegistration { + readonly onSubmit: (result: IChatPlanReviewResult) => void; + readonly items: IPlanReviewFeedbackItem[]; + navigationAnchor: string | undefined; +} + +export class PlanReviewFeedbackService extends Disposable implements IPlanReviewFeedbackService { + + declare readonly _serviceBrand: undefined; + + private readonly _registrations = new Map(); + + private readonly _onDidChangeFeedback = this._register(new Emitter()); + readonly onDidChangeFeedback: Event = this._onDidChangeFeedback.event; + + private readonly _onDidChangeNavigation = this._register(new Emitter()); + readonly onDidChangeNavigation: Event = this._onDidChangeNavigation.event; + + private readonly _onDidChangeRegistrations = this._register(new Emitter()); + readonly onDidChangeRegistrations: Event = this._onDidChangeRegistrations.event; + + registerPlanReview(planUri: URI, onSubmit: (result: IChatPlanReviewResult) => void): IDisposable { + const key = planUri.toString(); + this._registrations.set(key, { onSubmit, items: [], navigationAnchor: undefined }); + this._onDidChangeRegistrations.fire(); + return toDisposable(() => { + this._registrations.delete(key); + this._onDidChangeRegistrations.fire(); + }); + } + + isActivePlanReview(uri: URI): boolean { + return this._registrations.has(uri.toString()); + } + + addFeedback(planUri: URI, line: number, column: number, text: string): string { + const key = planUri.toString(); + const registration = this._registrations.get(key); + if (!registration) { + return ''; + } + + const id = generateUuid(); + registration.items.push({ id, line, column, text }); + // Keep items sorted by line number + registration.items.sort((a, b) => a.line - b.line || a.column - b.column); + this._onDidChangeFeedback.fire(planUri); + return id; + } + + removeFeedback(planUri: URI, feedbackId: string): void { + const key = planUri.toString(); + const registration = this._registrations.get(key); + if (!registration) { + return; + } + + const idx = registration.items.findIndex(item => item.id === feedbackId); + if (idx >= 0) { + registration.items.splice(idx, 1); + this._onDidChangeFeedback.fire(planUri); + } + } + + updateFeedback(planUri: URI, feedbackId: string, newText: string): void { + const key = planUri.toString(); + const registration = this._registrations.get(key); + if (!registration) { + return; + } + + const idx = registration.items.findIndex(item => item.id === feedbackId); + if (idx >= 0) { + const old = registration.items[idx]; + registration.items[idx] = { id: old.id, line: old.line, column: old.column, text: newText }; + this._onDidChangeFeedback.fire(planUri); + } + } + + getFeedback(planUri: URI): readonly IPlanReviewFeedbackItem[] { + const key = planUri.toString(); + return this._registrations.get(key)?.items ?? []; + } + + clearFeedback(planUri: URI): void { + const key = planUri.toString(); + const registration = this._registrations.get(key); + if (!registration || registration.items.length === 0) { + return; + } + registration.items.length = 0; + registration.navigationAnchor = undefined; + this._onDidChangeFeedback.fire(planUri); + } + + getNextFeedback(planUri: URI, next: boolean): IPlanReviewFeedbackItem | undefined { + const key = planUri.toString(); + const registration = this._registrations.get(key); + if (!registration || registration.items.length === 0) { + return undefined; + } + + const items = registration.items; + const anchorIdx = registration.navigationAnchor + ? items.findIndex(item => item.id === registration.navigationAnchor) + : -1; + + let targetIdx: number; + if (anchorIdx === -1) { + targetIdx = next ? 0 : items.length - 1; + } else { + targetIdx = next + ? (anchorIdx + 1) % items.length + : (anchorIdx - 1 + items.length) % items.length; + } + + const target = items[targetIdx]; + this.setNavigationAnchor(planUri, target.id); + return target; + } + + getNavigationBearing(planUri: URI): { activeIdx: number; totalCount: number } { + const key = planUri.toString(); + const registration = this._registrations.get(key); + if (!registration) { + return { activeIdx: -1, totalCount: 0 }; + } + + const totalCount = registration.items.length; + if (!registration.navigationAnchor) { + return { activeIdx: -1, totalCount }; + } + + const activeIdx = registration.items.findIndex(item => item.id === registration.navigationAnchor); + return { activeIdx, totalCount }; + } + + setNavigationAnchor(planUri: URI, itemId: string | undefined): void { + const key = planUri.toString(); + const registration = this._registrations.get(key); + if (registration) { + registration.navigationAnchor = itemId; + this._onDidChangeNavigation.fire(planUri); + } + } + + submitAllFeedback(planUri: URI): void { + const key = planUri.toString(); + const registration = this._registrations.get(key); + if (!registration || registration.items.length === 0) { + return; + } + + const formatted = this._formatFeedback(registration.items); + registration.onSubmit({ rejected: false, feedback: formatted }); + } + + private _formatFeedback(items: readonly IPlanReviewFeedbackItem[]): string { + const parts: string[] = ['Here\'s the feedback:']; + for (const item of items) { + if (item.column > 1) { + parts.push(`Line ${item.line}: Column ${item.column}: ${item.text}`); + } else { + parts.push(`Line ${item.line}: ${item.text}`); + } + } + return parts.join('\n'); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPlanReviewPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPlanReviewPart.ts index 5825d0f7dcbe0..4b35a3c2107a6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPlanReviewPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPlanReviewPart.ts @@ -26,6 +26,7 @@ import { IMarkdownRendererService } from '../../../../../../platform/markdown/br import { defaultButtonStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { IChatPlanApprovalAction, IChatPlanReview, IChatPlanReviewResult } from '../../../common/chatService/chatService.js'; +import { IPlanReviewFeedbackItem, IPlanReviewFeedbackService } from '../../planReviewFeedback/planReviewFeedbackService.js'; import { ChatPlanReviewData } from '../../../common/model/chatProgressTypes/chatPlanReviewData.js'; import { IChatRendererContent, isResponseVM } from '../../../common/model/chatViewModel.js'; import { ChatTreeItem } from '../../chat.js'; @@ -60,6 +61,7 @@ export class ChatPlanReviewPart extends Disposable implements IChatContentPart { private _feedbackTextarea: HTMLTextAreaElement | undefined; private _feedbackSection: HTMLElement | undefined; private _isFeedbackMode = false; + private readonly _planReviewRegistration = this._register(new MutableDisposable()); constructor( public readonly review: IChatPlanReview, @@ -70,6 +72,7 @@ export class ChatPlanReviewPart extends Disposable implements IChatContentPart { @IDialogService private readonly _dialogService: IDialogService, @IEditorService private readonly _editorService: IEditorService, @IHoverService private readonly _hoverService: IHoverService, + @IPlanReviewFeedbackService private readonly _planReviewFeedbackService: IPlanReviewFeedbackService, ) { super(); @@ -82,6 +85,22 @@ export class ChatPlanReviewPart extends Disposable implements IChatContentPart { const isResponseComplete = isResponseVM(context.element) && context.element.isComplete; this._isSubmitted = !!review.isUsed || isResponseComplete; + // Register with the plan review feedback service so the editor + // contribution can show inline feedback input for this plan file. + // Only register when feedback is allowed and the review hasn't + // already been submitted. + if (review.planUri && review.canProvideFeedback && !this._isSubmitted) { + const planUri = URI.revive(review.planUri); + this._planReviewRegistration.value = this._planReviewFeedbackService.registerPlanReview(planUri, (result) => { + if (this._isSubmitted) { + return; + } + this._isSubmitted = true; + this._options.onSubmit(result); + this.markUsed(); + }); + } + // Build DOM that mirrors chat-confirmation-widget2 so we inherit its // styling (title bar, scrollable message, blue/grey button row). const elements = dom.h('.chat-confirmation-widget-container.chat-plan-review-container@container', [ @@ -424,10 +443,37 @@ export class ChatPlanReviewPart extends Disposable implements IChatContentPart { if (this._isSubmitted) { return; } - const feedback = this._feedbackTextarea?.value.trim(); - if (!feedback) { + const textareaFeedback = this._feedbackTextarea?.value.trim(); + + // Collect any inline editor feedback for this plan file. + let editorFeedbackItems: readonly IPlanReviewFeedbackItem[] = []; + if (this.review.planUri) { + const planUri = URI.revive(this.review.planUri); + editorFeedbackItems = this._planReviewFeedbackService.getFeedback(planUri); + } + + if (!textareaFeedback && editorFeedbackItems.length === 0) { return; } + + // Merge textarea feedback with editor-collected inline feedback. + const feedbackParts: string[] = []; + if (textareaFeedback) { + feedbackParts.push(textareaFeedback); + } + if (editorFeedbackItems.length > 0) { + const filePath = this.review.planUri?.path ? `(${this.review.planUri.path})` : ''; + feedbackParts.push(`Here's the feedback for contents of the plan file ${filePath}:`); + for (const item of editorFeedbackItems) { + if (item.column > 1) { + feedbackParts.push(`Line ${item.line}: Column ${item.column}: ${item.text}`); + } else { + feedbackParts.push(`Line ${item.line}: ${item.text}`); + } + } + } + + const feedback = feedbackParts.join('\n'); this._isSubmitted = true; this._options.onSubmit({ rejected: false, feedback }); this.markUsed(); @@ -460,6 +506,9 @@ export class ChatPlanReviewPart extends Disposable implements IChatContentPart { private markUsed(): void { this.domNode.classList.add('chat-plan-review-used'); this._buttonStore.clear(); + // Unregister from the feedback service so the editor contribution + // hides/disables immediately, even if the plan file is still open. + this._planReviewRegistration.clear(); if (this._feedbackTextarea) { this._feedbackTextarea.disabled = true; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts index d9beb3af8bb52..52f140bfee005 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts @@ -50,8 +50,8 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IActionWidgetService actionWidgetService: IActionWidgetService, - @IKeybindingService keybindingService: IKeybindingService, - @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, ) { super(undefined, action); @@ -63,13 +63,13 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { 'chat.queuePickerPrimary', isSteerDefault ? localize('chat.steerWithMessage', "Steer with Message") : localize('chat.queueMessage', "Add to Queue"), ThemeIcon.asClassName(isSteerDefault ? Codicon.arrowUp : Codicon.add), - !!contextKeyService.getContextKeyValue(ChatContextKeys.inputHasText.key), + !!this.contextKeyService.getContextKeyValue(ChatContextKeys.inputHasText.key), () => this._runDefaultAction() )); this._primaryAction = this._register(new ActionViewItem(undefined, this._primaryActionAction, { icon: true, label: false })); - this._register(contextKeyService.onDidChangeContext(e => { - this._primaryActionAction.enabled = !!contextKeyService.getContextKeyValue(ChatContextKeys.inputHasText.key); + this._register(this.contextKeyService.onDidChangeContext(e => { + this._primaryActionAction.enabled = !!this.contextKeyService.getContextKeyValue(ChatContextKeys.inputHasText.key); })); // Dropdown - action widget with hover descriptions and chevron-down icon @@ -81,8 +81,8 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { showItemKeybindings: true, }, actionWidgetService, - keybindingService, - contextKeyService, + this.keybindingService, + this.contextKeyService, telemetryService, )); @@ -176,6 +176,24 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { private _getDropdownActions(): IActionWidgetDropdownAction[] { const isSteerDefault = this._isSteerDefault(); + // Resolve display keybindings against an overlay context that simulates the chat input + // being focused with a request in progress. The injected `contextKeyService` may be the + // chat widget's outer scope (which has `requestInProgress` but lacks `inChatInput`) or + // even the global scope; either way, the queue/steer keybindings' `when` clauses would + // not match and the resolver would fall back to the last-registered binding for each + // command, producing labels that contradict actual behavior. Overlaying the scope keys + // the bindings expect ensures we look up the binding that would actually fire. + // Other context keys (e.g. `editingRequestType`, `config.chat.requestQueuing.defaultAction`) + // are read from the parent context, so user customizations and scoped overrides like + // editing a queued/steer request are still respected. + const lookupContext = this.contextKeyService.createOverlay([ + [ChatContextKeys.inputHasText.key, true], + [ChatContextKeys.inChatInput.key, true], + [ChatContextKeys.requestInProgress.key, true], + ]); + const queueKeybinding = this.keybindingService.lookupKeybinding(ChatQueueMessageAction.ID, lookupContext, true); + const steerKeybinding = this.keybindingService.lookupKeybinding(ChatSteerWithMessageAction.ID, lookupContext, true); + const queueAction: IActionWidgetDropdownAction = { id: ChatQueueMessageAction.ID, label: localize('chat.queueMessage', "Add to Queue"), @@ -184,6 +202,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { checked: !isSteerDefault, icon: Codicon.add, class: undefined, + keybinding: queueKeybinding, hover: { content: localize('chat.queueMessage.hover', "Queue this message to send after the current request completes. The current response will finish uninterrupted before the queued message is sent."), }, @@ -200,6 +219,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { checked: isSteerDefault, icon: Codicon.arrowUp, class: undefined, + keybinding: steerKeybinding, hover: { content: localize('chat.steerWithMessage.hover', "Send this message at the next opportunity, signaling the current request to yield. The current response will stop and the new message will be sent immediately."), }, 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 05f8a04272fa6..0b419e07c820e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -861,6 +861,7 @@ have to be updated for changes to the rules above, or to support more deeply nes /* top padding is inside the editor widget */ width: 100%; position: relative; + transition: box-shadow 350ms ease; } /* Prevent contents from covering border corners. Not applied in compact mode @@ -873,10 +874,14 @@ have to be updated for changes to the rules above, or to support more deeply nes /* Animated gradient border shown around the chat input while the agent is working or thinking. Toggled by the `chat.progressBorder.enabled` - setting and the chat widget's request-in-progress state. The ring is - rendered using layered `background-image` + `background-clip` - (`padding-box`/`border-box`), so it traces the input's outer corner - radius and isn't clipped by `overflow: hidden` on the parent. */ + setting and the chat widget's request-in-progress state. + + The ring is rendered as a `::before` pseudo-element so it can fade in + and out via `opacity` when the `.working` class is toggled, without + disturbing the input's own background. The pseudo uses a 1px padding + + inverted mask trick to paint a hairline gradient ring that follows + the input's corner radius. The three color stops are themeable via + `chat.inputWorkingBorderColor1/2/3`. */ @property --chat-input-anim-angle { syntax: ''; inherits: false; @@ -893,46 +898,74 @@ have to be updated for changes to the rules above, or to support more deeply nes } } +/* Halo that cycles through the same three theme colors as the conic ring, + in sync with the 3s spin, so the glow appears to emanate from the + rotating gradient. */ @keyframes chat-input-working-border-glow { + 0% { + box-shadow: + 0 0 10px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 22%, transparent), + 0 0 22px 2px color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 10%, transparent); + } - 0%, - 100% { + 33% { box-shadow: - 0 0 4px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 18%, transparent), - 0 0 10px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor3) 8%, transparent); + 0 0 10px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor2) 22%, transparent), + 0 0 22px 2px color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor2) 10%, transparent); } - 50% { + 66% { box-shadow: - 0 0 6px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor2) 22%, transparent), - 0 0 14px 1px color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 12%, transparent); + 0 0 10px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor3) 22%, transparent), + 0 0 22px 2px color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor3) 10%, transparent); } + + 100% { + box-shadow: + 0 0 10px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 22%, transparent), + 0 0 22px 2px color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 10%, transparent); + } +} + +.monaco-workbench .interactive-session .chat-input-container::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: conic-gradient(from var(--chat-input-anim-angle), + var(--vscode-chat-inputWorkingBorderColor1), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor3), + var(--vscode-chat-inputWorkingBorderColor2), + var(--vscode-chat-inputWorkingBorderColor1)); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity 350ms ease; + pointer-events: none; + z-index: 1; } .monaco-workbench .interactive-session .chat-input-container.working { border-color: transparent; - /* The padding-box layer fills the input interior. It defaults to the - standard input background, but each host can override - `--chat-input-working-fill` to keep its own background color (e.g. the - Sessions workbench sets it to `--vscode-agentsChatInput-background`). - The conic-gradient layer is clipped to the border box so it paints - exactly where the (transparent) border lives. */ - background: - linear-gradient(var(--chat-input-working-fill, var(--vscode-input-background)), - var(--chat-input-working-fill, var(--vscode-input-background))) padding-box, - conic-gradient(from var(--chat-input-anim-angle), - var(--vscode-chat-inputWorkingBorderColor1), - var(--vscode-chat-inputWorkingBorderColor2), - var(--vscode-chat-inputWorkingBorderColor3), - var(--vscode-chat-inputWorkingBorderColor2), - var(--vscode-chat-inputWorkingBorderColor1)) border-box; - animation: - chat-input-working-border-spin 1.2s linear infinite, - chat-input-working-border-glow 3s ease-in-out infinite; + animation: chat-input-working-border-glow 3s linear infinite; +} + +.monaco-workbench .interactive-session .chat-input-container.working::before { + opacity: 1; + animation: chat-input-working-border-spin 3s linear infinite; } @media (prefers-reduced-motion: reduce) { - .monaco-workbench .interactive-session .chat-input-container.working { + .monaco-workbench .interactive-session .chat-input-container.working, + .monaco-workbench .interactive-session .chat-input-container.working::before { animation: none; } } diff --git a/src/vs/workbench/contrib/chat/test/browser/actions/chatQueueActions.test.ts b/src/vs/workbench/contrib/chat/test/browser/actions/chatQueueActions.test.ts new file mode 100644 index 0000000000000..c00f652588fe4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/actions/chatQueueActions.test.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 assert from 'assert'; +import { OS } from '../../../../../../base/common/platform.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ContextKeyService } from '../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { KeybindingsRegistry } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { KeybindingResolver } from '../../../../../../platform/keybinding/common/keybindingResolver.js'; +import { ResolvedKeybindingItem } from '../../../../../../platform/keybinding/common/resolvedKeybindingItem.js'; +import { USLayoutResolvedKeybinding } from '../../../../../../platform/keybinding/common/usLayoutResolvedKeybinding.js'; +import { ChatQueueMessageAction, ChatSteerWithMessageAction, registerChatQueueActions } from '../../../browser/actions/chatQueueActions.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; +import { ChatConfiguration } from '../../../common/constants.js'; + +// Register actions once so the keybindings appear in KeybindingsRegistry. +registerChatQueueActions(); + +suite('Queue/Steer keybinding resolution', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + function buildResolverForCommands(commandIds: string[]): KeybindingResolver { + const items: ResolvedKeybindingItem[] = []; + for (const item of KeybindingsRegistry.getDefaultKeybindingsForOS(OS)) { + if (!item.command || !commandIds.includes(item.command) || !item.keybinding) { + continue; + } + const resolved = USLayoutResolvedKeybinding.resolveKeybinding(item.keybinding, OS)[0]; + items.push(new ResolvedKeybindingItem(resolved, item.command, item.commandArgs, item.when ?? undefined, true, null, false)); + } + return new KeybindingResolver(items, [], () => { }); + } + + function lookupForConfig(defaultAction: 'steer' | 'queue') { + const config = new TestConfigurationService({ [ChatConfiguration.RequestQueueingDefaultAction]: defaultAction }); + const ctxService = new ContextKeyService(config); + // Simulate the chat input being focused with a request in progress, like the picker does. + const overlay = ctxService.createOverlay([ + [ChatContextKeys.inputHasText.key, true], + [ChatContextKeys.inChatInput.key, true], + [ChatContextKeys.requestInProgress.key, true], + ]); + const resolver = buildResolverForCommands([ChatQueueMessageAction.ID, ChatSteerWithMessageAction.ID]); + return { + result: { + queue: resolver.lookupPrimaryKeybinding(ChatQueueMessageAction.ID, overlay, true)?.resolvedKeybinding?.getDispatchChords()[0] ?? null, + steer: resolver.lookupPrimaryKeybinding(ChatSteerWithMessageAction.ID, overlay, true)?.resolvedKeybinding?.getDispatchChords()[0] ?? null, + }, + dispose: () => ctxService.dispose(), + }; + } + + test('with default=steer, Enter steers and Alt+Enter queues', () => { + const { result, dispose } = lookupForConfig('steer'); + try { + assert.deepStrictEqual(result, { queue: 'alt+Enter', steer: 'Enter' }); + } finally { + dispose(); + } + }); + + test('with default=queue, Enter queues and Alt+Enter steers', () => { + const { result, dispose } = lookupForConfig('queue'); + try { + assert.deepStrictEqual(result, { queue: 'Enter', steer: 'alt+Enter' }); + } finally { + dispose(); + } + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/planReviewFeedbackService.test.ts b/src/vs/workbench/contrib/chat/test/browser/planReviewFeedbackService.test.ts new file mode 100644 index 0000000000000..89f10dc5c576d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/planReviewFeedbackService.test.ts @@ -0,0 +1,356 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { IPlanReviewFeedbackService, PlanReviewFeedbackService } from '../../browser/planReviewFeedback/planReviewFeedbackService.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; + +function feedbackSummary(items: readonly { line: number; column: number }[]): string[] { + return items.map(f => `${f.line}:${f.column}`); +} + +suite('PlanReviewFeedbackService - Ordering', () => { + + const store = new DisposableStore(); + let service: IPlanReviewFeedbackService; + let planUri: URI; + + setup(() => { + service = store.add(new PlanReviewFeedbackService()); + planUri = URI.parse('file:///plan.md'); + store.add(service.registerPlanReview(planUri, () => { })); + }); + + teardown(() => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('items sorted by line number', () => { + service.addFeedback(planUri, 20, 1, 'line 20'); + service.addFeedback(planUri, 5, 1, 'line 5'); + service.addFeedback(planUri, 10, 1, 'line 10'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(planUri)), [ + '5:1', + '10:1', + '20:1', + ]); + }); + + test('items sorted by line then column', () => { + service.addFeedback(planUri, 10, 20, 'col 20'); + service.addFeedback(planUri, 10, 5, 'col 5'); + service.addFeedback(planUri, 10, 10, 'col 10'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(planUri)), [ + '10:5', + '10:10', + '10:20', + ]); + }); + + test('removing feedback preserves ordering', () => { + const id1 = service.addFeedback(planUri, 30, 1, 'line 30'); + service.addFeedback(planUri, 10, 1, 'line 10'); + service.addFeedback(planUri, 20, 1, 'line 20'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(planUri)), [ + '10:1', + '20:1', + '30:1', + ]); + + service.removeFeedback(planUri, id1); + assert.deepStrictEqual(feedbackSummary(service.getFeedback(planUri)), [ + '10:1', + '20:1', + ]); + }); + + test('same line number items are stable', () => { + const id1 = service.addFeedback(planUri, 10, 1, 'first'); + const id2 = service.addFeedback(planUri, 10, 1, 'second'); + + const items = service.getFeedback(planUri); + assert.strictEqual(items[0].id, id1); + assert.strictEqual(items[1].id, id2); + }); + + test('clear removes all items', () => { + service.addFeedback(planUri, 1, 1, 'a'); + service.addFeedback(planUri, 2, 1, 'b'); + service.addFeedback(planUri, 3, 1, 'c'); + + assert.strictEqual(service.getFeedback(planUri).length, 3); + service.clearFeedback(planUri); + assert.strictEqual(service.getFeedback(planUri).length, 0); + }); + + test('update feedback changes text', () => { + const id = service.addFeedback(planUri, 10, 1, 'original'); + service.updateFeedback(planUri, id, 'updated'); + + const items = service.getFeedback(planUri); + assert.strictEqual(items.length, 1); + assert.strictEqual(items[0].text, 'updated'); + assert.strictEqual(items[0].line, 10); + }); +}); + +suite('PlanReviewFeedbackService - Navigation', () => { + + const store = new DisposableStore(); + let service: IPlanReviewFeedbackService; + let planUri: URI; + + setup(() => { + service = store.add(new PlanReviewFeedbackService()); + planUri = URI.parse('file:///plan.md'); + store.add(service.registerPlanReview(planUri, () => { })); + }); + + teardown(() => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('navigation follows sorted order', () => { + service.addFeedback(planUri, 20, 1, 'line 20'); + service.addFeedback(planUri, 5, 1, 'line 5'); + service.addFeedback(planUri, 10, 1, 'line 10'); + + // Expected order: 5, 10, 20 + const first = service.getNextFeedback(planUri, true)!; + assert.strictEqual(first.line, 5); + + const second = service.getNextFeedback(planUri, true)!; + assert.strictEqual(second.line, 10); + + const third = service.getNextFeedback(planUri, true)!; + assert.strictEqual(third.line, 20); + + // Wraps around + const fourth = service.getNextFeedback(planUri, true)!; + assert.strictEqual(fourth.line, 5); + }); + + test('navigation backwards', () => { + service.addFeedback(planUri, 5, 1, 'line 5'); + service.addFeedback(planUri, 10, 1, 'line 10'); + service.addFeedback(planUri, 20, 1, 'line 20'); + + // First backward nav goes to last item + const first = service.getNextFeedback(planUri, false)!; + assert.strictEqual(first.line, 20); + + const second = service.getNextFeedback(planUri, false)!; + assert.strictEqual(second.line, 10); + + const third = service.getNextFeedback(planUri, false)!; + assert.strictEqual(third.line, 5); + + // Wraps around + const fourth = service.getNextFeedback(planUri, false)!; + assert.strictEqual(fourth.line, 20); + }); + + test('navigation bearings reflect sorted position', () => { + service.addFeedback(planUri, 20, 1, 'line 20'); + service.addFeedback(planUri, 5, 1, 'line 5'); + service.addFeedback(planUri, 10, 1, 'line 10'); + + // Before navigation, no anchor + let bearing = service.getNavigationBearing(planUri); + assert.strictEqual(bearing.activeIdx, -1); + assert.strictEqual(bearing.totalCount, 3); + + // Navigate to first (5) + service.getNextFeedback(planUri, true); + bearing = service.getNavigationBearing(planUri); + assert.strictEqual(bearing.activeIdx, 0); + + // Navigate to second (10) + service.getNextFeedback(planUri, true); + bearing = service.getNavigationBearing(planUri); + assert.strictEqual(bearing.activeIdx, 1); + + // Navigate to third (20) + service.getNextFeedback(planUri, true); + bearing = service.getNavigationBearing(planUri); + assert.strictEqual(bearing.activeIdx, 2); + }); + + test('navigation returns undefined for empty feedback', () => { + const result = service.getNextFeedback(planUri, true); + assert.strictEqual(result, undefined); + }); + + test('setNavigationAnchor updates the anchor', () => { + const id = service.addFeedback(planUri, 10, 1, 'line 10'); + service.addFeedback(planUri, 20, 1, 'line 20'); + + service.setNavigationAnchor(planUri, id); + const bearing = service.getNavigationBearing(planUri); + assert.strictEqual(bearing.activeIdx, 0); + }); +}); + +suite('PlanReviewFeedbackService - Registration', () => { + + const store = new DisposableStore(); + let service: IPlanReviewFeedbackService; + + setup(() => { + service = store.add(new PlanReviewFeedbackService()); + }); + + teardown(() => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('isActivePlanReview returns false before registration', () => { + const planUri = URI.parse('file:///plan.md'); + assert.strictEqual(service.isActivePlanReview(planUri), false); + }); + + test('isActivePlanReview returns true after registration', () => { + const planUri = URI.parse('file:///plan.md'); + store.add(service.registerPlanReview(planUri, () => { })); + assert.strictEqual(service.isActivePlanReview(planUri), true); + }); + + test('isActivePlanReview returns false after dispose', () => { + const planUri = URI.parse('file:///plan.md'); + const registration = service.registerPlanReview(planUri, () => { }); + assert.strictEqual(service.isActivePlanReview(planUri), true); + registration.dispose(); + assert.strictEqual(service.isActivePlanReview(planUri), false); + }); + + test('feedback cannot be added to unregistered plan', () => { + const planUri = URI.parse('file:///plan.md'); + const id = service.addFeedback(planUri, 1, 1, 'text'); + assert.strictEqual(id, ''); + assert.strictEqual(service.getFeedback(planUri).length, 0); + }); + + test('dispose clears feedback items', () => { + const planUri = URI.parse('file:///plan.md'); + const registration = service.registerPlanReview(planUri, () => { }); + service.addFeedback(planUri, 1, 1, 'text'); + assert.strictEqual(service.getFeedback(planUri).length, 1); + registration.dispose(); + assert.strictEqual(service.getFeedback(planUri).length, 0); + }); + + test('onDidChangeRegistrations fires on register and dispose', () => { + const planUri = URI.parse('file:///plan.md'); + let fireCount = 0; + store.add(service.onDidChangeRegistrations(() => fireCount++)); + + const registration = service.registerPlanReview(planUri, () => { }); + assert.strictEqual(fireCount, 1); + + registration.dispose(); + assert.strictEqual(fireCount, 2); + }); + + test('onDidChangeFeedback fires on add and remove', () => { + const planUri = URI.parse('file:///plan.md'); + store.add(service.registerPlanReview(planUri, () => { })); + + let fireCount = 0; + store.add(service.onDidChangeFeedback(() => fireCount++)); + + const id = service.addFeedback(planUri, 1, 1, 'text'); + assert.strictEqual(fireCount, 1); + + service.removeFeedback(planUri, id); + assert.strictEqual(fireCount, 2); + }); +}); + +suite('PlanReviewFeedbackService - Submit', () => { + + const store = new DisposableStore(); + let service: IPlanReviewFeedbackService; + + setup(() => { + service = store.add(new PlanReviewFeedbackService()); + }); + + teardown(() => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('submitAllFeedback calls onSubmit with formatted feedback', () => { + const planUri = URI.parse('file:///plan.md'); + let submittedResult: { rejected: boolean; feedback?: string } | undefined; + store.add(service.registerPlanReview(planUri, (result) => { submittedResult = result; })); + + service.addFeedback(planUri, 1, 1, 'fix this'); + service.addFeedback(planUri, 45, 45, 'change that'); + + service.submitAllFeedback(planUri); + + assert.ok(submittedResult); + assert.strictEqual(submittedResult!.rejected, false); + assert.strictEqual(submittedResult!.feedback, [ + 'Here\'s the feedback:', + 'Line 1: fix this', + 'Line 45: Column 45: change that', + ].join('\n')); + }); + + test('submitAllFeedback does nothing when no items', () => { + const planUri = URI.parse('file:///plan.md'); + let called = false; + store.add(service.registerPlanReview(planUri, () => { called = true; })); + + service.submitAllFeedback(planUri); + assert.strictEqual(called, false); + }); + + test('feedback at column 1 omits column', () => { + const planUri = URI.parse('file:///plan.md'); + let submittedResult: { feedback?: string } | undefined; + store.add(service.registerPlanReview(planUri, (result) => { submittedResult = result; })); + + service.addFeedback(planUri, 10, 1, 'at start'); + + service.submitAllFeedback(planUri); + + assert.ok(submittedResult); + assert.strictEqual(submittedResult!.feedback, [ + 'Here\'s the feedback:', + 'Line 10: at start', + ].join('\n')); + }); + + test('feedback at column > 1 includes column', () => { + const planUri = URI.parse('file:///plan.md'); + let submittedResult: { feedback?: string } | undefined; + store.add(service.registerPlanReview(planUri, (result) => { submittedResult = result; })); + + service.addFeedback(planUri, 10, 15, 'mid line'); + + service.submitAllFeedback(planUri); + + assert.ok(submittedResult); + assert.strictEqual(submittedResult!.feedback, [ + 'Here\'s the feedback:', + 'Line 10: Column 15: mid line', + ].join('\n')); + }); +}); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 866c7bc8d3380..6a10c81e6bee4 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -15,7 +15,6 @@ import { MenuId, MenuRegistry } from '../../../../platform/actions/common/action import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { KeybindingWeight, KeybindingsRegistry } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../platform/quickinput/common/quickAccess.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; @@ -28,11 +27,10 @@ import { IViewContainersRegistry, IViewsRegistry, ViewContainer, ViewContainerLo import { launchSchemaId } from '../../../services/configuration/common/configuration.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { COPY_NOTEBOOK_VARIABLE_VALUE_ID, COPY_NOTEBOOK_VARIABLE_VALUE_LABEL } from '../../notebook/browser/contrib/notebookVariables/notebookVariableCommands.js'; -import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_UX, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_THREADS_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_VARIABLE_VALUE, CONTEXT_WATCH_ITEM_TYPE, DEBUG_PANEL_ID, DISASSEMBLY_VIEW_ID, EDITOR_CONTRIBUTION_ID, IDebugService, INTERNAL_CONSOLE_OPTIONS_SCHEMA, LOADED_SCRIPTS_VIEW_ID, REPL_VIEW_ID, State, VARIABLES_VIEW_ID, VIEWLET_ID, WATCH_VIEW_ID, getStateLabel } from '../common/debug.js'; +import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_UX, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_THREADS_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_VARIABLE_VALUE, CONTEXT_WATCH_ITEM_TYPE, DEBUG_PANEL_ID, DISASSEMBLY_VIEW_ID, EDITOR_CONTRIBUTION_ID, INTERNAL_CONSOLE_OPTIONS_SCHEMA, LOADED_SCRIPTS_VIEW_ID, REPL_VIEW_ID, State, VARIABLES_VIEW_ID, VIEWLET_ID, WATCH_VIEW_ID, getStateLabel } from '../common/debug.js'; import { DebugWatchAccessibilityAnnouncer } from '../common/debugAccessibilityAnnouncer.js'; import { DebugContentProvider } from '../common/debugContentProvider.js'; import { DebugLifecycle } from '../common/debugLifecycle.js'; -import { DebugVisualizerService, IDebugVisualizerService } from '../common/debugVisualizers.js'; import { DisassemblyViewInput } from '../common/disassemblyViewInput.js'; import { ReplAccessibilityAnnouncer } from '../common/replAccessibilityAnnouncer.js'; import { BreakpointEditorContribution } from './breakpointEditorContribution.js'; @@ -47,7 +45,6 @@ import { DebugEditorContribution } from './debugEditorContribution.js'; import * as icons from './debugIcons.js'; import { DebugProgressContribution } from './debugProgress.js'; import { StartDebugQuickAccessProvider } from './debugQuickAccess.js'; -import { DebugService } from './debugService.js'; import './debugSettingMigration.js'; import { DebugStatusContribution } from './debugStatus.js'; import { DebugTitleContribution } from './debugTitle.js'; @@ -67,10 +64,12 @@ import { ADD_WATCH_ID, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REM import { WelcomeView } from './welcomeView.js'; import { DebugChatContextContribution } from './debugChatIntegration.js'; +// Register services +import './debug.service.contribution.js'; + const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); -registerSingleton(IDebugService, DebugService, InstantiationType.Delayed); -registerSingleton(IDebugVisualizerService, DebugVisualizerService, InstantiationType.Delayed); + // Register Debug Workbench Contributions Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugStatusContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/debug/browser/debug.service.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.service.contribution.ts new file mode 100644 index 0000000000000..93038daa11fc4 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debug.service.contribution.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { DebugService } from './debugService.js'; +import { IDebugService } from '../common/debug.js'; +import { IDebugVisualizerService, DebugVisualizerService } from '../common/debugVisualizers.js'; + +registerSingleton(IDebugService, DebugService, InstantiationType.Delayed); +registerSingleton(IDebugVisualizerService, DebugVisualizerService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 2a2643375a2de..67c86483f14b8 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -531,6 +531,13 @@ const apiMenus: IAPIMenu[] = [ supportsSubmenus: false, proposed: 'chatSessionCustomizationProvider', }, + { + key: 'chat/customizations/item', + id: MenuId.for('AICustomizationManagementEditorItem'), + description: localize('menus.chatCustomizationsItem', "The item context menu in the Chat Customizations management editor, including inline actions."), + supportsSubmenus: false, + proposed: 'chatSessionCustomizationProvider', + }, { key: 'chat/editor/inlineGutter', id: MenuId.ChatEditorInlineMenu,