Merge remote-tracking branch 'origin/main' into tyriar/276071_contrib_terminal_common__other

This commit is contained in:
Daniel Imms
2025-11-07 11:06:50 -08:00
12 changed files with 117 additions and 34 deletions

View File

@@ -338,24 +338,6 @@ export default tseslint.config(
'src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts',
'src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts',
'src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts',
// 'src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts',
// 'src/vs/workbench/contrib/terminal/browser/remotePty.ts',
// 'src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalActions.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalGroup.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalIcon.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalInstance.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalMenus.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalService.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts',
// 'src/vs/workbench/contrib/terminal/browser/terminalView.ts',
// 'src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts',
'src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts',
'src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts',
'src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts',

View File

@@ -7,7 +7,7 @@
import { Model } from '../model';
import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions } from './git';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode';
import { combinedDisposable, filterEvent, mapEvent } from '../util';
import { toGitUri } from '../uri';
@@ -163,6 +163,10 @@ export class ApiRepository implements Repository {
return this.#repository.diffWithHEAD(path);
}
diffWithHEADShortStats(path?: string): Promise<CommitShortStat> {
return this.#repository.diffWithHEADShortStats(path);
}
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string): Promise<string | Change[]> {

View File

@@ -237,6 +237,7 @@ export interface Repository {
diff(cached?: boolean): Promise<string>;
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEADShortStats(path?: string): Promise<CommitShortStat>;
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffIndexWithHEAD(): Promise<Change[]>;

View File

@@ -1628,6 +1628,10 @@ export class Repository {
return result.stdout;
}
async diffWithHEADShortStats(path?: string): Promise<CommitShortStat> {
return this.diffFilesShortStat(undefined, { cached: false, path });
}
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
@@ -1717,6 +1721,32 @@ export class Repository {
return parseGitChanges(this.repositoryRoot, gitResult.stdout);
}
private async diffFilesShortStat(ref: string | undefined, options: { cached: boolean; path?: string }): Promise<CommitShortStat> {
const args = ['diff', '--shortstat'];
if (options.cached) {
args.push('--cached');
}
if (ref !== undefined) {
args.push(ref);
}
args.push('--');
if (options.path) {
args.push(this.sanitizeRelativePath(options.path));
}
const result = await this.exec(args);
if (result.exitCode) {
return { files: 0, insertions: 0, deletions: 0 };
}
return parseGitDiffShortStat(result.stdout.trim());
}
async diffTrees(treeish1: string, treeish2?: string, options?: { similarityThreshold?: number }): Promise<Change[]> {
const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR'];

View File

@@ -14,7 +14,7 @@ import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode
import { AutoFetcher } from './autofetch';
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
import { debounce, memoize, sequentialize, throttle } from './decorators';
import { Repository as BaseRepository, BlameInformation, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git';
import { Repository as BaseRepository, BlameInformation, Commit, CommitShortStat, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git';
import { GitHistoryProvider } from './historyProvider';
import { Operation, OperationKind, OperationManager, OperationResult } from './operation';
import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands';
@@ -1207,6 +1207,10 @@ export class Repository implements Disposable {
return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path));
}
diffWithHEADShortStats(path?: string): Promise<CommitShortStat> {
return this.run(Operation.Diff, () => this.repository.diffWithHEADShortStats(path));
}
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getAllCodicons } from '../../../base/common/codicons.js';
import { Codicon, getAllCodicons } from '../../../base/common/codicons.js';
import { IJSONSchema, IJSONSchemaMap } from '../../../base/common/jsonSchema.js';
import { OperatingSystem, Platform, PlatformToString } from '../../../base/common/platform.js';
import { localize } from '../../../nls.js';
@@ -175,7 +175,7 @@ const terminalPlatformConfiguration: IConfigurationNode = {
default: {
'PowerShell': {
source: 'PowerShell',
icon: 'terminal-powershell'
icon: Codicon.terminalPowershell.id,
},
'Command Prompt': {
path: [
@@ -183,10 +183,11 @@ const terminalPlatformConfiguration: IConfigurationNode = {
'${env:windir}\\System32\\cmd.exe'
],
args: [],
icon: 'terminal-cmd'
icon: Codicon.terminalCmd,
},
'Git Bash': {
source: 'Git Bash'
source: 'Git Bash',
icon: Codicon.terminalGitBash.id,
}
},
additionalProperties: {
@@ -234,7 +235,7 @@ const terminalPlatformConfiguration: IConfigurationNode = {
'bash': {
path: 'bash',
args: ['-l'],
icon: 'terminal-bash'
icon: Codicon.terminalBash.id
},
'zsh': {
path: 'zsh',
@@ -246,11 +247,11 @@ const terminalPlatformConfiguration: IConfigurationNode = {
},
'tmux': {
path: 'tmux',
icon: 'terminal-tmux'
icon: Codicon.terminalTmux.id
},
'pwsh': {
path: 'pwsh',
icon: 'terminal-powershell'
icon: Codicon.terminalPowershell.id
}
},
additionalProperties: {
@@ -286,7 +287,7 @@ const terminalPlatformConfiguration: IConfigurationNode = {
default: {
'bash': {
path: 'bash',
icon: 'terminal-bash'
icon: Codicon.terminalBash.id
},
'zsh': {
path: 'zsh'
@@ -296,11 +297,11 @@ const terminalPlatformConfiguration: IConfigurationNode = {
},
'tmux': {
path: 'tmux',
icon: 'terminal-tmux'
icon: Codicon.terminalTmux.id
},
'pwsh': {
path: 'pwsh',
icon: 'terminal-powershell'
icon: Codicon.terminalPowershell.id
}
},
additionalProperties: {

View File

@@ -106,6 +106,7 @@ async function detectAvailableWindowsProfiles(
});
detectedProfiles.set('Git Bash', {
source: ProfileSource.GitBash,
icon: Codicon.terminalGitBash,
isAutoDetected: true
});
detectedProfiles.set('Command Prompt', {

View File

@@ -128,6 +128,9 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS
if (terminalCustomActions) {
moreActions.push(...terminalCustomActions);
}
if (moreActions.length === 0) {
moreActions = undefined;
}
}
const codeBlockRenderOptions: ICodeBlockRenderOptions = {

View File

@@ -24,7 +24,18 @@ export interface ICommandLineAnalyzerOptions {
}
export interface ICommandLineAnalyzerResult {
/**
* Whether auto approval is allowed based on the analysis, when false this
* will block auto approval.
*/
readonly isAutoApproveAllowed: boolean;
/**
* Whether the command line was explicitly auto approved by this analyzer.
* - `true`: This analyzer explicitly approves auto-execution
* - `false`: This analyzer explicitly denies auto-execution
* - `undefined`: This analyzer does not make an approval/denial decision
*/
readonly isAutoApproved?: boolean;
readonly disclaimers?: readonly string[];
readonly autoApproveInfo?: IMarkdownString;
readonly customActions?: ToolConfirmationAction[];

View File

@@ -155,7 +155,9 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
}
return {
isAutoApproveAllowed: isAutoApproved,
isAutoApproved,
// This is not based on isDenied because we want the user to be able to configure it
isAutoApproveAllowed: true,
disclaimers,
autoApproveInfo,
customActions,

View File

@@ -417,15 +417,29 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
disclaimer = new MarkdownString(`$(${Codicon.info.id}) ` + disclaimersRaw.join(' '), { supportThemeIcons: true });
}
const customActions = commandLineAnalyzerResults.map(e => e.customActions ?? []).flat();
toolSpecificData.autoApproveInfo = commandLineAnalyzerResults.find(e => e.autoApproveInfo)?.autoApproveInfo;
const analyzersIsAutoApproveAllowed = commandLineAnalyzerResults.every(e => e.isAutoApproveAllowed);
const customActions = analyzersIsAutoApproveAllowed ? commandLineAnalyzerResults.map(e => e.customActions ?? []).flat() : undefined;
let shellType = basename(shell, '.exe');
if (shellType === 'powershell') {
shellType = 'pwsh';
}
const isFinalAutoApproved = isAutoApproveAllowed && commandLineAnalyzerResults.every(e => e.isAutoApproveAllowed);
const isFinalAutoApproved = (
// Is the setting enabled and the user has opted-in
isAutoApproveAllowed &&
// Does at least one analyzer auto approve
commandLineAnalyzerResults.some(e => e.isAutoApproved) &&
// No analyzer denies auto approval
commandLineAnalyzerResults.every(e => e.isAutoApproved !== false) &&
// All analyzers allow auto approval
analyzersIsAutoApproveAllowed
);
if (isFinalAutoApproved) {
toolSpecificData.autoApproveInfo = commandLineAnalyzerResults.find(e => e.autoApproveInfo)?.autoApproveInfo;
}
const confirmationMessages = isFinalAutoApproved ? undefined : {
title: args.isBackground
? localize('runInTerminal.background', "Run `{0}` command? (background terminal)", shellType)

View File

@@ -35,6 +35,10 @@ import { arch } from '../../../../../../base/common/process.js';
import { URI } from '../../../../../../base/common/uri.js';
import { LocalChatSessionUri } from '../../../../chat/common/chatUri.js';
import type { SingleOrMany } from '../../../../../../base/common/types.js';
import { IWorkspaceContextService, toWorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js';
import { IHistoryService } from '../../../../../services/history/common/history.js';
import { TestContextService } from '../../../../../test/common/workbenchTestServices.js';
import { Workspace } from '../../../../../../platform/workspace/test/common/testWorkspace.js';
class TestRunInTerminalTool extends RunInTerminalTool {
protected override _osBackend: Promise<OperatingSystem> = Promise.resolve(OperatingSystem.Windows);
@@ -55,6 +59,7 @@ class TestRunInTerminalTool extends RunInTerminalTool {
let configurationService: TestConfigurationService;
let fileService: IFileService;
let storageService: IStorageService;
let workspaceContextService: TestContextService;
let terminalServiceDisposeEmitter: Emitter<ITerminalInstance>;
let chatServiceDisposeEmitter: Emitter<{ sessionResource: URI; reason: 'cleared' }>;
@@ -62,6 +67,7 @@ class TestRunInTerminalTool extends RunInTerminalTool {
setup(() => {
configurationService = new TestConfigurationService();
workspaceContextService = new TestContextService();
const logService = new NullLogService();
fileService = store.add(new FileService(logService));
@@ -77,6 +83,11 @@ class TestRunInTerminalTool extends RunInTerminalTool {
fileService: () => fileService,
}, store);
instantiationService.stub(IWorkspaceContextService, workspaceContextService);
instantiationService.stub(IHistoryService, {
getLastActiveWorkspaceRoot: () => undefined
});
const treeSitterLibraryService = store.add(instantiationService.createInstance(TreeSitterLibraryService));
treeSitterLibraryService.isTest = true;
instantiationService.stub(ITreeSitterLibraryService, treeSitterLibraryService);
@@ -839,6 +850,25 @@ class TestRunInTerminalTool extends RunInTerminalTool {
'configure',
]);
});
test('should prevent auto approval when writing to a file outside the workspace', async () => {
setConfig(TerminalChatAgentToolsSettingId.BlockDetectedFileWrites, 'outsideWorkspace');
setAutoApprove({});
const workspaceFolder = URI.file(isWindows ? 'C:/workspace/project' : '/workspace/project');
const workspace = new Workspace('test', [toWorkspaceFolder(workspaceFolder)]);
workspaceContextService.setWorkspace(workspace);
instantiationService.stub(IHistoryService, {
getLastActiveWorkspaceRoot: () => workspaceFolder
});
const result = await executeToolTest({
command: 'echo "abc" > ../file.txt'
});
assertConfirmationRequired(result);
strictEqual(result?.confirmationMessages?.terminalCustomActions, undefined, 'Expected no custom actions when file write is blocked');
});
});
suite('chat session disposal cleanup', () => {