mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-29 18:09:01 +01:00
Merge branch 'main' into mrleemurray/tragic-gayal-turquoise
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"portsAttributes",
|
||||
"quickInputButtonLocation",
|
||||
"quickPickSortByLabel",
|
||||
"quickPickItemResource",
|
||||
"resolvers",
|
||||
"scmActionButton",
|
||||
"scmSelectedProvider",
|
||||
|
||||
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -28,6 +28,6 @@ export const enum MarshalledId {
|
||||
LanguageModelThinkingPart,
|
||||
LanguageModelPromptTsxPart,
|
||||
LanguageModelDataPart,
|
||||
ChatSessionContext,
|
||||
AgentSessionContext,
|
||||
ChatResponsePullRequestPart,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export interface IChatCollapsibleIOCodePart {
|
||||
languageId: string;
|
||||
options: ICodeBlockRenderOptions;
|
||||
codeBlockInfo: IChatCodeBlockInfo;
|
||||
title?: string | IMarkdownString;
|
||||
}
|
||||
|
||||
export interface IChatCollapsibleIODataPart {
|
||||
|
||||
+18
-2
@@ -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),
|
||||
|
||||
+2
-128
@@ -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;
|
||||
|
||||
|
||||
+1
@@ -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'] = {
|
||||
|
||||
+12
-2
@@ -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'}`);
|
||||
|
||||
+1
@@ -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> {
|
||||
|
||||
+17
-2
@@ -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) {
|
||||
|
||||
+12
-2
@@ -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) {
|
||||
|
||||
+27
@@ -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;
|
||||
}
|
||||
|
||||
+49
-22
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+44
-23
@@ -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);
|
||||
|
||||
Vendored
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user