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