Merge branch 'main' into mrleemurray/tragic-gayal-turquoise

This commit is contained in:
Lee Murray
2025-12-17 09:39:01 +00:00
committed by GitHub
68 changed files with 1166 additions and 467 deletions
+184
View File
@@ -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
@@ -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
+5 -6
View File
@@ -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"
}
],
@@ -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,
-1
View File
@@ -34,7 +34,6 @@
"portsAttributes",
"quickInputButtonLocation",
"quickPickSortByLabel",
"quickPickItemResource",
"resolvers",
"scmActionButton",
"scmSelectedProvider",
Binary file not shown.
+2
View File
@@ -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;
+1 -1
View File
@@ -28,6 +28,6 @@ export const enum MarshalledId {
LanguageModelThinkingPart,
LanguageModelPromptTsxPart,
LanguageModelDataPart,
ChatSessionContext,
AgentSessionContext,
ChatResponsePullRequestPart,
}
+8 -4
View File
@@ -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<VSBuffer>());
private readonly _onClose = this._register(new Emitter<SocketCloseEvent>());
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;
}
}
@@ -84,6 +84,7 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens
}
const upgraded = upgradeToISocket(req, socket as Socket, {
debugLabel: 'extension-host-cdp-' + generateUuid(),
enableMessageSplitting: false,
});
if (upgraded) {
@@ -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',
},
@@ -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;
@@ -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) {
@@ -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 });
}
@@ -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,
@@ -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);
@@ -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;
}
@@ -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)
}
});
}
@@ -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,
@@ -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<IAgentSessionsFilter> {
@@ -29,7 +29,7 @@ export interface IAgentSessionsFilterOptions extends Partial<IAgentSessionsFilte
interface IAgentSessionsViewExcludes {
readonly providers: readonly string[];
readonly states: readonly ChatSessionStatus[];
readonly states: readonly AgentSessionStatus[];
readonly archived: boolean;
readonly read: boolean;
@@ -165,11 +165,11 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
}
private registerStateActions(disposables: DisposableStore): void {
const states: { id: ChatSessionStatus; label: string }[] = [
{ id: ChatSessionStatus.Completed, label: localize('chatSessionStatus.completed', "Completed") },
{ id: ChatSessionStatus.InProgress, label: localize('chatSessionStatus.inProgress', "In Progress") },
{ id: ChatSessionStatus.NeedsInput, label: localize('chatSessionStatus.needsInput', "Input Needed") },
{ id: ChatSessionStatus.Failed, label: localize('chatSessionStatus.failed', "Failed") },
const states: { id: AgentSessionStatus; label: string }[] = [
{ id: AgentSessionStatus.Completed, label: localize('agentSessionStatus.completed', "Completed") },
{ id: AgentSessionStatus.InProgress, label: localize('agentSessionStatus.inProgress', "In Progress") },
{ id: AgentSessionStatus.NeedsInput, label: localize('agentSessionStatus.needsInput', "Input Needed") },
{ id: AgentSessionStatus.Failed, label: localize('agentSessionStatus.failed', "Failed") },
];
const that = this;
@@ -10,17 +10,21 @@ import { Emitter, Event } from '../../../../../base/common/event.js';
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../../base/common/map.js';
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI, UriComponents } from '../../../../../base/common/uri.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
import { ChatSessionStatus, IChatSessionFileChange, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, localChatSessionType } from '../../common/chatSessionsService.js';
import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js';
import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js';
//#region Interfaces, Types
export { ChatSessionStatus as AgentSessionStatus } from '../../common/chatSessionsService.js';
export { isSessionInProgressStatus } from '../../common/chatSessionsService.js';
export interface IAgentSessionsModel {
readonly onWillResolve: Event<void>;
@@ -34,14 +38,14 @@ export interface IAgentSessionsModel {
resolve(provider: string | string[] | undefined): Promise<void>;
}
interface IAgentSessionData {
interface IAgentSessionData extends Omit<IChatSessionItem, 'archived' | 'iconPath'> {
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<string | undefined>();
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<IAgentSessionData, 'iconPath' | 'resource' | 'icon'> {
readonly providerType: string;
readonly providerLabel: string;
readonly resource: UriComponents;
readonly status: ChatSessionStatus;
readonly status: AgentSessionStatus;
readonly tooltip?: string | IMarkdownString;
@@ -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<IAgentSes
}
private getIcon(session: IAgentSession): ThemeIcon {
if (session.status === ChatSessionStatus.InProgress) {
if (session.status === AgentSessionStatus.InProgress) {
return Codicon.sessionInProgress;
}
if (session.status === ChatSessionStatus.NeedsInput) {
if (session.status === AgentSessionStatus.NeedsInput) {
return Codicon.report;
}
if (session.status === ChatSessionStatus.Failed) {
if (session.status === AgentSessionStatus.Failed) {
return Codicon.error;
}
@@ -276,9 +275,9 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
else {
if (isSessionInProgressStatus(session.element.status)) {
template.description.textContent = localize('chat.session.status.inProgress', "Working...");
} else if (session.element.status === ChatSessionStatus.NeedsInput) {
} else if (session.element.status === AgentSessionStatus.NeedsInput) {
template.description.textContent = localize('chat.session.status.needsInput', "Input needed.");
} else if (hasBadge && session.element.status === ChatSessionStatus.Completed) {
} else if (hasBadge && session.element.status === AgentSessionStatus.Completed) {
template.description.textContent = ''; // no description if completed and has badge
} else if (
session.element.timing.finishedOrFailedTime &&
@@ -287,11 +286,11 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
) {
const duration = this.toDuration(session.element.timing.inProgressTime, session.element.timing.finishedOrFailedTime);
template.description.textContent = session.element.status === ChatSessionStatus.Failed ?
template.description.textContent = session.element.status === AgentSessionStatus.Failed ?
localize('chat.session.status.failedAfter', "Failed after {0}.", duration ?? '1s') :
localize('chat.session.status.completedAfter', "Finished in {0}.", duration ?? '1s');
} else {
template.description.textContent = session.element.status === ChatSessionStatus.Failed ?
template.description.textContent = session.element.status === AgentSessionStatus.Failed ?
localize('chat.session.status.failed', "Failed") :
localize('chat.session.status.completed', "Finished");
}
@@ -466,13 +465,13 @@ export class AgentSessionsAccessibilityProvider implements IListAccessibilityPro
let statusLabel: string;
switch (element.status) {
case ChatSessionStatus.NeedsInput:
case AgentSessionStatus.NeedsInput:
statusLabel = localize('agentSessionNeedsInput', "needs input");
break;
case ChatSessionStatus.InProgress:
case AgentSessionStatus.InProgress:
statusLabel = localize('agentSessionInProgress', "in progress");
break;
case ChatSessionStatus.Failed:
case AgentSessionStatus.Failed:
statusLabel = localize('agentSessionFailed', "failed");
break;
default:
@@ -686,8 +685,8 @@ export class AgentSessionsSorter implements ITreeSorter<IAgentSession> {
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<IAgentSession> {
}
// 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)
+11 -1
View File
@@ -45,6 +45,11 @@ export interface IChatWidgetService {
readonly onDidAddWidget: Event<IChatWidget>;
/**
* Fires when a chat session is no longer open in any chat widget.
*/
readonly onDidBackgroundSession: Event<URI>;
/**
* 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<void>;
readonly onDidChangeViewModel: Event<IChatWidgetViewModelChangeEvent>;
readonly onDidAcceptInput: Event<void>;
readonly onDidHide: Event<void>;
readonly onDidShow: Event<void>;
@@ -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,
};
}
@@ -33,6 +33,7 @@ export interface IChatCollapsibleIOCodePart {
languageId: string;
options: ICodeBlockRenderOptions;
codeBlockInfo: IChatCodeBlockInfo;
title?: string | IMarkdownString;
}
export interface IChatCollapsibleIODataPart {
@@ -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<CodeBlockPart>[] = [];
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),
@@ -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<IDetachedTerminalInstance> | 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<void> {
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<void>(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<IDetachedTerminalInstance> {
if (!this._detachedTerminal) {
this._detachedTerminal = this._createTerminal();
}
return this._detachedTerminal;
}
private async _createTerminal(): Promise<IDetachedTerminalInstance> {
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;
@@ -120,6 +120,7 @@ export class ChatToolPostExecuteConfirmationPart extends AbstractToolConfirmatio
parts.push({
kind: 'code',
title: part.title,
textModel: model,
languageId: model.getLanguageId(),
options: {
@@ -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) {
@@ -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<void>());
readonly onDidFocus = this._onDidFocus.event;
private _onDidChangeViewModel = this._register(new Emitter<void>());
private _onDidChangeViewModel = this._register(new Emitter<IChatWidgetViewModelChangeEvent>());
readonly onDidChangeViewModel = this._onDidChangeViewModel.event;
private _onDidScroll = this._register(new Emitter<void>());
@@ -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() {
@@ -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<IChatWidget>());
readonly onDidAddWidget = this._onDidAddWidget.event;
private readonly _onDidBackgroundSession = this._register(new Emitter<URI>());
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))
);
}
@@ -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',
@@ -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;
}
@@ -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;
}
@@ -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<string[]>(uniqueUrls.map(u => [u, extractUrlPatterns(u)] as const));
@@ -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 {
@@ -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<ChatRequestVariableSet> {
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;
}
}
@@ -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);
}
@@ -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));
});
});
@@ -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');
});
});
@@ -11,6 +11,7 @@ import { ChatAgentLocation } from '../../common/constants.js';
export class MockChatWidgetService implements IChatWidgetService {
readonly onDidAddWidget: Event<IChatWidget> = Event.None;
readonly onDidBackgroundSession: Event<URI> = Event.None;
readonly _serviceBrand: undefined;
@@ -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
@@ -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;
}
@@ -46,7 +46,7 @@ suite('McpStdioStateHandler', () => {
process.on('SIGTERM', () => process.stdout.write('SIGTERM received'));
`);
child.stdin.write('Hello MCP!');
await new Promise<void>(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));
});
});
`);
@@ -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,
@@ -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[]> | Task[], placeHolder: string, defaultEntry?: ITaskQuickPickEntry, group: boolean = false, sort: boolean = false, selectedEntry?: ITaskQuickPickEntry, additionalEntries?: ITaskQuickPickEntry[], name?: string): Promise<ITaskQuickPickEntry | undefined | null> {
@@ -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+(.*)$/,
@@ -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);
});
});
@@ -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<IDetachedTerminalInstance> | undefined;
private _attachedContainer: HTMLElement | undefined;
protected _setDetachedTerminal(detachedTerminal: Promise<IDetachedTerminalInstance>): void {
this._detachedTerminal = detachedTerminal.then(terminal => this._register(terminal));
}
protected async _getTerminal(): Promise<IDetachedTerminalInstance> {
if (!this._detachedTerminal) {
throw new Error('Detached terminal not initialized');
}
return this._detachedTerminal;
}
protected async _attachToContainer(container: HTMLElement): Promise<IDetachedTerminalInstance> {
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<IDetachedTerminalInstance>;
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<void> {
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<void>(resolve => {
@@ -131,30 +185,102 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach
});
return { lineCount: vt.lineCount };
}
}
private async _createTerminal(): Promise<IDetachedTerminalInstance> {
/**
* 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<void> {
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<void>(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;
}
}
}
@@ -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();
}
@@ -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(
@@ -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();
}
@@ -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(
@@ -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'] = {
@@ -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'}`);
@@ -29,6 +29,7 @@ export interface ITerminalExecuteStrategyResult {
additionalInformation?: string;
exitCode?: number;
error?: string;
didEnterAltBuffer?: boolean;
}
export async function waitForIdle(onData: Event<unknown>, idleDurationMs: number): Promise<void> {
@@ -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) {
@@ -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) {
@@ -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<void> {
const deferred = new DeferredPromise<void>();
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;
}
@@ -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`);
@@ -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<boolean>;
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<boolean> {
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<boolean> {
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;
}
}
@@ -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<void> {
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<HTMLElement | undefined> {
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;
}
}
@@ -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);
}
}
@@ -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);
+25
View File
@@ -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.
*/
@@ -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;
}
}
-27
View File
@@ -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<T extends QuickPickItem> 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;
}
}
@@ -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