mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-21 23:59:34 +01:00
Merge branch 'main' into joh/esbuild-the-things
This commit is contained in:
@@ -312,10 +312,10 @@ export class DarwinTestRunner extends PosixTestRunner {
|
||||
|
||||
/** @override */
|
||||
protected override async binaryPath() {
|
||||
const { nameLong } = await this.readProductJson();
|
||||
const { nameLong, nameShort } = await this.readProductJson();
|
||||
return path.join(
|
||||
this.repoLocation.uri.fsPath,
|
||||
`.build/electron/${nameLong}.app/Contents/MacOS/Electron`
|
||||
`.build/electron/${nameLong}.app/Contents/MacOS/${nameShort}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+3
-3
@@ -505,7 +505,7 @@
|
||||
"request": "launch",
|
||||
"name": "Run Unit Tests",
|
||||
"program": "${workspaceFolder}/test/unit/electron/index.js",
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Electron",
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Code - OSS",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.exe"
|
||||
},
|
||||
@@ -535,7 +535,7 @@
|
||||
"request": "launch",
|
||||
"name": "Run Unit Tests For Current File",
|
||||
"program": "${workspaceFolder}/test/unit/electron/index.js",
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Electron",
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Code - OSS",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.exe"
|
||||
},
|
||||
@@ -571,7 +571,7 @@
|
||||
"timeout": 240000,
|
||||
"args": [
|
||||
"-l",
|
||||
"${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Electron"
|
||||
"${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Code - OSS"
|
||||
],
|
||||
"outFiles": [
|
||||
"${cwd}/out/**/*.js"
|
||||
|
||||
Vendored
-2
@@ -24,11 +24,9 @@
|
||||
"files.insertFinalNewline": false
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[rust]": {
|
||||
|
||||
@@ -98,6 +98,20 @@ jobs:
|
||||
DEBUG=* node build/darwin/create-universal-app.ts $(agent.builddirectory)
|
||||
displayName: Create Universal App
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
APP_ROOT="$(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH)"
|
||||
APP_NAME="`ls $APP_ROOT | head -n 1`"
|
||||
APP_PATH="$APP_ROOT/$APP_NAME"
|
||||
EXEC_NAME=$(node -p "require(\"$APP_PATH/Contents/Resources/app/product.json\").nameShort")
|
||||
# Create a symlink from 'Electron' to the actual executable for backward compatibility
|
||||
# This ensures apps that relied on the hardcoded path 'Contents/MacOS/Electron' continue to work
|
||||
# Remove this step once main branch is on 1.112 release.
|
||||
if [ "$EXEC_NAME" != "Electron" ] && [ ! -L "$APP_PATH/Contents/MacOS/Electron" ]; then
|
||||
ln -s "$EXEC_NAME" "$APP_PATH/Contents/MacOS/Electron"
|
||||
fi
|
||||
displayName: Create Electron symlink for backward compatibility
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
APP_ROOT="$(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH)"
|
||||
|
||||
@@ -166,6 +166,21 @@ steps:
|
||||
chmod +x "$APP_PATH/Contents/Resources/app/bin/$CLI_APP_NAME"
|
||||
displayName: Make CLI executable
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
APP_ROOT="$(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH)"
|
||||
APP_NAME="`ls $APP_ROOT | head -n 1`"
|
||||
APP_PATH="$APP_ROOT/$APP_NAME"
|
||||
EXEC_NAME=$(node -p "require(\"$APP_PATH/Contents/Resources/app/product.json\").nameShort")
|
||||
# Create a symlink from 'Electron' to the actual executable for backward compatibility
|
||||
# This ensures apps that relied on the hardcoded path 'Contents/MacOS/Electron' continue to work
|
||||
# Remove this step once main branch is on 1.112 release.
|
||||
if [ "$EXEC_NAME" != "Electron" ] && [ ! -L "$APP_PATH/Contents/MacOS/Electron" ]; then
|
||||
ln -s "$EXEC_NAME" "$APP_PATH/Contents/MacOS/Electron"
|
||||
fi
|
||||
condition: eq(variables['BUILT_CLIENT'], 'true')
|
||||
displayName: Create Electron symlink for backward compatibility
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
APP_ROOT="$(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH)"
|
||||
|
||||
@@ -58,7 +58,9 @@ steps:
|
||||
set -e
|
||||
APP_ROOT="$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)"
|
||||
APP_NAME="`ls $APP_ROOT | head -n 1`"
|
||||
INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \
|
||||
ProductJsonPath=$(find "$APP_ROOT" -name "product.json" -type f | head -n 1)
|
||||
BINARY_NAME=$(jq -r '.nameShort' "$ProductJsonPath")
|
||||
INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/$BINARY_NAME" \
|
||||
./scripts/test-integration.sh --build --tfs "Integration Tests"
|
||||
env:
|
||||
VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH)
|
||||
@@ -77,7 +79,9 @@ steps:
|
||||
set -e
|
||||
APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)
|
||||
APP_NAME="`ls $APP_ROOT | head -n 1`"
|
||||
INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \
|
||||
ProductJsonPath=$(find "$APP_ROOT" -name "product.json" -type f | head -n 1)
|
||||
BINARY_NAME=$(jq -r '.nameShort' "$ProductJsonPath")
|
||||
INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/$BINARY_NAME" \
|
||||
./scripts/test-remote-integration.sh
|
||||
env:
|
||||
VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH)
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
# Check if package.json or package-lock.json are in the changed files
|
||||
if echo "$CHANGED_FILES" | grep -E '^(package\.json|package-lock\.json)$'; then
|
||||
if echo "$CHANGED_FILES" | grep -E '(^|/)package(-lock)?\.json$'; then
|
||||
echo "##vso[task.setvariable variable=SHOULD_VALIDATE]true"
|
||||
echo "Package files were modified, proceeding with validation"
|
||||
else
|
||||
|
||||
@@ -61,6 +61,12 @@ const RULES: IRule[] = [
|
||||
disallowedTypes: NATIVE_TYPES,
|
||||
},
|
||||
|
||||
// Browser view preload script
|
||||
{
|
||||
target: '**/vs/platform/browserView/electron-browser/preload-browserView.ts',
|
||||
disallowedTypes: NATIVE_TYPES,
|
||||
},
|
||||
|
||||
// Common
|
||||
{
|
||||
target: '**/vs/**/common/**',
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"../../src/**/test/**",
|
||||
"../../src/**/fixtures/**",
|
||||
"../../src/vs/base/parts/sandbox/electron-browser/preload.ts", // Preload scripts for Electron sandbox
|
||||
"../../src/vs/base/parts/sandbox/electron-browser/preload-aux.ts" // have limited access to node.js APIs
|
||||
"../../src/vs/base/parts/sandbox/electron-browser/preload-aux.ts", // have limited access to node.js APIs
|
||||
"../../src/vs/platform/browserView/electron-browser/preload-browserView.ts" // Browser view preload script
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,21 +7,22 @@
|
||||
import { EventEmitter } from 'events';
|
||||
EventEmitter.defaultMaxListeners = 100;
|
||||
|
||||
import es from 'event-stream';
|
||||
import glob from 'glob';
|
||||
import gulp from 'gulp';
|
||||
import filter from 'gulp-filter';
|
||||
import plumber from 'gulp-plumber';
|
||||
import sourcemaps from 'gulp-sourcemaps';
|
||||
import * as path from 'path';
|
||||
import * as nodeUtil from 'util';
|
||||
import es from 'event-stream';
|
||||
import filter from 'gulp-filter';
|
||||
import * as util from './lib/util.ts';
|
||||
import { getVersion } from './lib/getVersion.ts';
|
||||
import * as task from './lib/task.ts';
|
||||
import watcher from './lib/watch/index.ts';
|
||||
import { createReporter } from './lib/reporter.ts';
|
||||
import glob from 'glob';
|
||||
import plumber from 'gulp-plumber';
|
||||
import * as ext from './lib/extensions.ts';
|
||||
import { getVersion } from './lib/getVersion.ts';
|
||||
import { createReporter } from './lib/reporter.ts';
|
||||
import * as task from './lib/task.ts';
|
||||
import * as tsb from './lib/tsb/index.ts';
|
||||
import sourcemaps from 'gulp-sourcemaps';
|
||||
import { createTsgoStream, spawnTsgo } from './lib/tsgo.ts';
|
||||
import * as util from './lib/util.ts';
|
||||
import watcher from './lib/watch/index.ts';
|
||||
|
||||
const root = path.dirname(import.meta.dirname);
|
||||
const commit = getVersion(root);
|
||||
@@ -78,6 +79,18 @@ const compilations = [
|
||||
|
||||
const getBaseUrl = (out: string) => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`;
|
||||
|
||||
function rewriteTsgoSourceMappingUrlsIfNeeded(build: boolean, out: string, baseUrl: string): Promise<void> {
|
||||
if (!build) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return util.streamToPromise(
|
||||
gulp.src(path.join(out, '**', '*.js'), { base: out })
|
||||
.pipe(util.rewriteSourceMappingURL(baseUrl))
|
||||
.pipe(gulp.dest(out))
|
||||
);
|
||||
}
|
||||
|
||||
const tasks = compilations.map(function (tsconfigFile) {
|
||||
const absolutePath = path.join(root, tsconfigFile);
|
||||
const relativeDirname = path.dirname(tsconfigFile.replace(/^(.*\/)?extensions\//i, ''));
|
||||
@@ -150,25 +163,22 @@ const tasks = compilations.map(function (tsconfigFile) {
|
||||
.pipe(gulp.dest(out));
|
||||
}));
|
||||
|
||||
const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, () => {
|
||||
const pipeline = createPipeline(false, true);
|
||||
const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts']));
|
||||
const input = es.merge(nonts, pipeline.tsProjectSrc());
|
||||
const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, async () => {
|
||||
const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true }));
|
||||
const copyNonTs = util.streamToPromise(nonts.pipe(gulp.dest(out)));
|
||||
const tsgo = spawnTsgo(absolutePath, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl));
|
||||
|
||||
return input
|
||||
.pipe(pipeline())
|
||||
.pipe(gulp.dest(out));
|
||||
await Promise.all([copyNonTs, tsgo]);
|
||||
}));
|
||||
|
||||
const watchTask = task.define(`watch-extension:${name}`, task.series(cleanTask, () => {
|
||||
const pipeline = createPipeline(false);
|
||||
const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts']));
|
||||
const input = es.merge(nonts, pipeline.tsProjectSrc());
|
||||
const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true }));
|
||||
const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } });
|
||||
const watchNonTs = watchInput.pipe(filter(['**', '!**/*.ts'], { dot: true })).pipe(gulp.dest(out));
|
||||
const tsgoStream = watchInput.pipe(util.debounce(() => createTsgoStream(absolutePath, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)), 200));
|
||||
const watchStream = es.merge(nonts.pipe(gulp.dest(out)), watchNonTs, tsgoStream);
|
||||
|
||||
return watchInput
|
||||
.pipe(util.incremental(pipeline, input))
|
||||
.pipe(gulp.dest(out));
|
||||
return watchStream;
|
||||
}));
|
||||
|
||||
// Tasks
|
||||
|
||||
+8
-10
@@ -3,6 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { EventEmitter } from 'events';
|
||||
EventEmitter.defaultMaxListeners = 100;
|
||||
|
||||
import glob from 'glob';
|
||||
import gulp from 'gulp';
|
||||
import { createRequire } from 'node:module';
|
||||
@@ -12,29 +14,25 @@ import * as compilation from './lib/compilation.ts';
|
||||
import * as task from './lib/task.ts';
|
||||
import * as util from './lib/util.ts';
|
||||
|
||||
EventEmitter.defaultMaxListeners = 100;
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const { transpileTask, compileTask, watchTask, compileApiProposalNamesTask, watchApiProposalNamesTask } = compilation;
|
||||
|
||||
// API proposal names
|
||||
gulp.task(compileApiProposalNamesTask);
|
||||
gulp.task(watchApiProposalNamesTask);
|
||||
gulp.task(compilation.compileApiProposalNamesTask);
|
||||
gulp.task(compilation.watchApiProposalNamesTask);
|
||||
|
||||
// SWC Client Transpile
|
||||
const transpileClientSWCTask = task.define('transpile-client-esbuild', task.series(util.rimraf('out'), transpileTask('src', 'out', true)));
|
||||
const transpileClientSWCTask = task.define('transpile-client-esbuild', task.series(util.rimraf('out'), compilation.transpileTask('src', 'out', true)));
|
||||
gulp.task(transpileClientSWCTask);
|
||||
|
||||
// Transpile only
|
||||
const transpileClientTask = task.define('transpile-client', task.series(util.rimraf('out'), transpileTask('src', 'out')));
|
||||
const transpileClientTask = task.define('transpile-client', task.series(util.rimraf('out'), compilation.transpileTask('src', 'out')));
|
||||
gulp.task(transpileClientTask);
|
||||
|
||||
// Fast compile for development time
|
||||
const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compileApiProposalNamesTask, compileTask('src', 'out', false)));
|
||||
const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compilation.compileApiProposalNamesTask, compilation.compileTask('src', 'out', false)));
|
||||
gulp.task(compileClientTask);
|
||||
|
||||
const watchClientTask = task.define('watch-client', task.series(util.rimraf('out'), task.parallel(watchTask('out', false), watchApiProposalNamesTask, compilation.watchCodiconsTask)));
|
||||
const watchClientTask = task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask)));
|
||||
gulp.task(watchClientTask);
|
||||
|
||||
// All
|
||||
|
||||
@@ -70,6 +70,7 @@ const vscodeResourceIncludes = [
|
||||
// Electron Preload
|
||||
'out-build/vs/base/parts/sandbox/electron-browser/preload.js',
|
||||
'out-build/vs/base/parts/sandbox/electron-browser/preload-aux.js',
|
||||
'out-build/vs/platform/browserView/electron-browser/preload-browserView.js',
|
||||
|
||||
// Node Scripts
|
||||
'out-build/vs/base/node/{terminateProcess.sh,cpuUsage.sh,ps.sh}',
|
||||
@@ -435,6 +436,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d
|
||||
} else if (platform === 'darwin') {
|
||||
const shortcut = gulp.src('resources/darwin/bin/code.sh')
|
||||
.pipe(replace('@@APPNAME@@', product.applicationName))
|
||||
.pipe(replace('@@NAME@@', product.nameShort))
|
||||
.pipe(rename('bin/code'));
|
||||
const policyDest = gulp.src('.build/policies/darwin/**', { base: '.build/policies/darwin' })
|
||||
.pipe(rename(f => f.dirname = `policies/${f.dirname}`));
|
||||
|
||||
@@ -109,6 +109,7 @@ export const config = {
|
||||
productAppName: product.nameLong,
|
||||
companyName: 'Microsoft Corporation',
|
||||
copyright: 'Copyright (C) 2026 Microsoft. All rights reserved',
|
||||
darwinExecutable: product.nameShort,
|
||||
darwinIcon: 'resources/darwin/code.icns',
|
||||
darwinBundleIdentifier: product.darwinBundleIdentifier,
|
||||
darwinApplicationCategoryType: 'public.app-category.developer-tools',
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import es from 'event-stream';
|
||||
import * as path from 'path';
|
||||
import { createReporter } from './reporter.ts';
|
||||
|
||||
const root = path.dirname(path.dirname(import.meta.dirname));
|
||||
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
||||
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
||||
|
||||
export function spawnTsgo(projectPath: string, onComplete?: () => Promise<void> | void): Promise<void> {
|
||||
const reporter = createReporter('extensions');
|
||||
let report: NodeJS.ReadWriteStream | undefined;
|
||||
|
||||
const beginReport = (emitError: boolean) => {
|
||||
if (report) {
|
||||
report.end();
|
||||
}
|
||||
report = reporter.end(emitError);
|
||||
};
|
||||
|
||||
const endReport = () => {
|
||||
if (!report) {
|
||||
return;
|
||||
}
|
||||
report.end();
|
||||
report = undefined;
|
||||
};
|
||||
|
||||
const args = ['tsgo', '--project', projectPath, '--pretty', 'false', '--sourceMap', '--inlineSources'];
|
||||
|
||||
beginReport(false);
|
||||
|
||||
const child = cp.spawn(npx, args, {
|
||||
cwd: root,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true
|
||||
});
|
||||
|
||||
let buffer = '';
|
||||
const handleLine = (line: string) => {
|
||||
const trimmed = line.replace(ansiRegex, '').trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
if (/Starting compilation|File change detected/i.test(trimmed)) {
|
||||
beginReport(false);
|
||||
return;
|
||||
}
|
||||
if (/Compilation complete/i.test(trimmed)) {
|
||||
endReport();
|
||||
return;
|
||||
}
|
||||
|
||||
const match = /(.*\(\d+,\d+\): )(.*: )(.*)/.exec(trimmed);
|
||||
|
||||
if (match) {
|
||||
const fullpath = path.isAbsolute(match[1]) ? match[1] : path.join(root, match[1]);
|
||||
const message = match[3];
|
||||
reporter(fullpath + message);
|
||||
} else {
|
||||
reporter(trimmed);
|
||||
}
|
||||
};
|
||||
|
||||
const handleData = (data: Buffer) => {
|
||||
buffer += data.toString('utf8');
|
||||
const lines = buffer.split(/\r?\n/);
|
||||
buffer = lines.pop() ?? '';
|
||||
for (const line of lines) {
|
||||
handleLine(line);
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout?.on('data', handleData);
|
||||
child.stderr?.on('data', handleData);
|
||||
|
||||
const done = new Promise<void>((resolve, reject) => {
|
||||
child.on('exit', code => {
|
||||
if (buffer.trim()) {
|
||||
handleLine(buffer);
|
||||
buffer = '';
|
||||
}
|
||||
endReport();
|
||||
if (code === 0) {
|
||||
Promise.resolve(onComplete?.()).then(() => resolve(), reject);
|
||||
return;
|
||||
}
|
||||
reject(new Error(`tsgo exited with code ${code ?? 'unknown'}`));
|
||||
});
|
||||
child.on('error', err => {
|
||||
endReport();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
return done;
|
||||
}
|
||||
|
||||
export function createTsgoStream(projectPath: string, onComplete?: () => Promise<void> | void): NodeJS.ReadWriteStream {
|
||||
const stream = es.through();
|
||||
|
||||
spawnTsgo(projectPath, onComplete).then(() => {
|
||||
stream.emit('end');
|
||||
}).catch(() => {
|
||||
// Errors are already reported by spawnTsgo via the reporter.
|
||||
// Don't emit 'error' on the stream as that would exit the watch process.
|
||||
stream.emit('end');
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -1603,7 +1603,7 @@ begin
|
||||
begin
|
||||
#ifdef AppxPackageName
|
||||
// Remove the old context menu registry keys
|
||||
if IsWindows11OrLater() and WizardIsTaskSelected('addcontextmenufiles') then begin
|
||||
if IsWindows11OrLater() then begin
|
||||
RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}');
|
||||
RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}');
|
||||
RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}');
|
||||
|
||||
@@ -47,11 +47,11 @@ function _throttle<T>(fn: Function, key: string): Function {
|
||||
return trigger;
|
||||
}
|
||||
|
||||
function decorate(decorator: (fn: Function, key: string) => Function): Function {
|
||||
return function (original: any, context: ClassMethodDecoratorContext) {
|
||||
if (context.kind !== 'method') {
|
||||
function decorate(decorator: (fn: Function, key: string) => Function): MethodDecorator {
|
||||
return (_target: any, key: string | symbol, descriptor: PropertyDescriptor): void => {
|
||||
if (typeof descriptor.value !== 'function') {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
return decorator(original, context.name.toString());
|
||||
descriptor.value = decorator(descriptor.value, String(key));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ import { ApiRepository, ApiImpl } from './api1';
|
||||
import { Event, EventEmitter } from 'vscode';
|
||||
import { CloneManager } from '../cloneManager';
|
||||
|
||||
function deprecated(original: unknown, context: ClassMemberDecoratorContext) {
|
||||
if (typeof original !== 'function' || context.kind !== 'method') {
|
||||
function deprecated(_target: unknown, key: string | symbol, descriptor: PropertyDescriptor): void {
|
||||
if (typeof descriptor.value !== 'function') {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
const key = context.name.toString();
|
||||
return function (this: unknown, ...args: unknown[]) {
|
||||
console.warn(`Git extension API method '${key}' is deprecated.`);
|
||||
const original = descriptor.value;
|
||||
descriptor.value = function (this: unknown, ...args: unknown[]) {
|
||||
console.warn(`Git extension API method '${String(key)}' is deprecated.`);
|
||||
return original.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -372,13 +372,12 @@ interface ScmCommand {
|
||||
|
||||
const Commands: ScmCommand[] = [];
|
||||
|
||||
function command(commandId: string, options: ScmCommandOptions = {}): Function {
|
||||
return (value: unknown, context: ClassMethodDecoratorContext) => {
|
||||
if (typeof value !== 'function' || context.kind !== 'method') {
|
||||
function command(commandId: string, options: ScmCommandOptions = {}): MethodDecorator {
|
||||
return (_target: any, key: string | symbol, descriptor: PropertyDescriptor): void => {
|
||||
if (typeof descriptor.value !== 'function') {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
const key = context.name.toString();
|
||||
Commands.push({ commandId, key, method: value, options });
|
||||
Commands.push({ commandId, key: String(key), method: descriptor.value, options });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
import { done } from './util';
|
||||
|
||||
function decorate(decorator: (fn: Function, key: string) => Function): Function {
|
||||
return function (original: unknown, context: ClassMethodDecoratorContext) {
|
||||
if (typeof original === 'function' && (context.kind === 'method' || context.kind === 'getter' || context.kind === 'setter')) {
|
||||
return decorator(original, context.name.toString());
|
||||
return (_target: any, key: string, descriptor: PropertyDescriptor): void => {
|
||||
if (typeof descriptor.value === 'function') {
|
||||
descriptor.value = decorator(descriptor.value, key);
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
descriptor.get = decorator(descriptor.get, key) as () => any;
|
||||
} else {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
throw new Error('not supported');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,5 +88,5 @@ export function debounce(delay: number): Function {
|
||||
clearTimeout(this[timerKey]);
|
||||
this[timerKey] = setTimeout(() => fn.apply(this, args), delay);
|
||||
};
|
||||
});
|
||||
}) as MethodDecorator;
|
||||
}
|
||||
|
||||
@@ -24,11 +24,14 @@ export class DisposableStore {
|
||||
}
|
||||
|
||||
function decorate(decorator: (fn: Function, key: string) => Function): Function {
|
||||
return function (original: any, context: ClassMethodDecoratorContext) {
|
||||
if (context.kind === 'method' || context.kind === 'getter' || context.kind === 'setter') {
|
||||
return decorator(original, context.name.toString());
|
||||
return (_target: any, key: string, descriptor: PropertyDescriptor): void => {
|
||||
if (typeof descriptor.value === 'function') {
|
||||
descriptor.value = decorator(descriptor.value, key);
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
descriptor.get = decorator(descriptor.get, key) as () => any;
|
||||
} else {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
throw new Error('not supported');
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
]
|
||||
],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
|
||||
@@ -540,6 +540,84 @@
|
||||
"settings": {
|
||||
"foreground": "#A8CAAD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Heading",
|
||||
"scope": "markup.heading",
|
||||
"settings": {
|
||||
"foreground": "#64b0df",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Bold",
|
||||
"scope": "markup.bold",
|
||||
"settings": {
|
||||
"foreground": "#C48081",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Italic",
|
||||
"scope": "markup.italic",
|
||||
"settings": {
|
||||
"fontStyle": "italic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Strikethrough",
|
||||
"scope": "markup.strikethrough",
|
||||
"settings": {
|
||||
"fontStyle": "strikethrough"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Underline",
|
||||
"scope": "markup.underline",
|
||||
"settings": {
|
||||
"fontStyle": "underline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Quote",
|
||||
"scope": "markup.quote",
|
||||
"settings": {
|
||||
"foreground": "#C184C6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup List",
|
||||
"scope": "markup.list",
|
||||
"settings": {
|
||||
"foreground": "#48C9C4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Inline Raw",
|
||||
"scope": "markup.inline.raw",
|
||||
"settings": {
|
||||
"foreground": "#D1D6AE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Raw/Fenced Code Block",
|
||||
"scope": [
|
||||
"markup.raw",
|
||||
"markup.fenced_code"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#888888"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Link",
|
||||
"scope": [
|
||||
"meta.link",
|
||||
"markup.underline.link"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#48A0C7"
|
||||
}
|
||||
}
|
||||
],
|
||||
"semanticHighlighting": true,
|
||||
|
||||
@@ -546,6 +546,84 @@
|
||||
"settings": {
|
||||
"foreground": "#2B9A69"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Heading",
|
||||
"scope": "markup.heading",
|
||||
"settings": {
|
||||
"foreground": "#5460C1",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Bold",
|
||||
"scope": "markup.bold",
|
||||
"settings": {
|
||||
"foreground": "#B86855",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Italic",
|
||||
"scope": "markup.italic",
|
||||
"settings": {
|
||||
"fontStyle": "italic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Strikethrough",
|
||||
"scope": "markup.strikethrough",
|
||||
"settings": {
|
||||
"fontStyle": "strikethrough"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Underline",
|
||||
"scope": "markup.underline",
|
||||
"settings": {
|
||||
"fontStyle": "underline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Quote",
|
||||
"scope": "markup.quote",
|
||||
"settings": {
|
||||
"foreground": "#8F41AD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup List",
|
||||
"scope": "markup.list",
|
||||
"settings": {
|
||||
"foreground": "#46969A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Inline Raw",
|
||||
"scope": "markup.inline.raw",
|
||||
"settings": {
|
||||
"foreground": "#98863B"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Raw/Fenced Code Block",
|
||||
"scope": [
|
||||
"markup.raw",
|
||||
"markup.fenced_code"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#666666"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Markup Link",
|
||||
"scope": [
|
||||
"meta.link",
|
||||
"markup.underline.link"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#0069CC"
|
||||
}
|
||||
}
|
||||
],
|
||||
"semanticHighlighting": true,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -20,7 +20,7 @@
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/policy-watcher": "^1.3.2",
|
||||
"@vscode/proxy-agent": "^0.37.0",
|
||||
"@vscode/proxy-agent": "^0.38.0",
|
||||
"@vscode/ripgrep": "^1.15.13",
|
||||
"@vscode/spdlog": "^0.15.7",
|
||||
"@vscode/sqlite3": "5.1.12-vscode",
|
||||
@@ -3364,9 +3364,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/proxy-agent": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.37.0.tgz",
|
||||
"integrity": "sha512-FDBc/3qf7fLMp4fmdRBav2dy3UZ/Vao4PN6a5IeTYvcgh9erd9HfOcVoU3ogy2uwCii6vZNvmEeF9+gr64spVQ==",
|
||||
"version": "0.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.38.0.tgz",
|
||||
"integrity": "sha512-f8fOGbYhCVG9FUbtVcL/90yjCyo6ZuuKQpA7hs7iIEMD8kesnoo04TUI3/29vifCZ2DCiyUN12CFgA+ktc2RTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tootallnate/once": "^3.0.0",
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.110.0",
|
||||
"distro": "6a427d4e06fa83b1b299fde50735094cb6562065",
|
||||
"distro": "56aa42f9c6baf6359f28840af043e27a1311455d",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
@@ -82,7 +82,7 @@
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/policy-watcher": "^1.3.2",
|
||||
"@vscode/proxy-agent": "^0.37.0",
|
||||
"@vscode/proxy-agent": "^0.38.0",
|
||||
"@vscode/ripgrep": "^1.15.13",
|
||||
"@vscode/spdlog": "^0.15.7",
|
||||
"@vscode/sqlite3": "5.1.12-vscode",
|
||||
|
||||
Generated
+4
-4
@@ -15,7 +15,7 @@
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/proxy-agent": "^0.37.0",
|
||||
"@vscode/proxy-agent": "^0.38.0",
|
||||
"@vscode/ripgrep": "^1.15.13",
|
||||
"@vscode/spdlog": "^0.15.7",
|
||||
"@vscode/tree-sitter-wasm": "^0.3.0",
|
||||
@@ -468,9 +468,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/proxy-agent": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.37.0.tgz",
|
||||
"integrity": "sha512-FDBc/3qf7fLMp4fmdRBav2dy3UZ/Vao4PN6a5IeTYvcgh9erd9HfOcVoU3ogy2uwCii6vZNvmEeF9+gr64spVQ==",
|
||||
"version": "0.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.38.0.tgz",
|
||||
"integrity": "sha512-f8fOGbYhCVG9FUbtVcL/90yjCyo6ZuuKQpA7hs7iIEMD8kesnoo04TUI3/29vifCZ2DCiyUN12CFgA+ktc2RTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tootallnate/once": "^3.0.0",
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/proxy-agent": "^0.37.0",
|
||||
"@vscode/proxy-agent": "^0.38.0",
|
||||
"@vscode/ripgrep": "^1.15.13",
|
||||
"@vscode/spdlog": "^0.15.7",
|
||||
"@vscode/tree-sitter-wasm": "^0.3.0",
|
||||
|
||||
@@ -29,7 +29,7 @@ if [ -z "$APP_PATH" ]; then
|
||||
exit 1
|
||||
fi
|
||||
CONTENTS="$APP_PATH/Contents"
|
||||
ELECTRON="$CONTENTS/MacOS/Electron"
|
||||
ELECTRON="$CONTENTS/MacOS/@@NAME@@"
|
||||
CLI="$CONTENTS/Resources/app/out/cli.js"
|
||||
export VSCODE_NODE_OPTIONS=$NODE_OPTIONS
|
||||
export VSCODE_NODE_REPL_EXTERNAL_MODULE=$NODE_REPL_EXTERNAL_MODULE
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ function code() {
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
NAME=`node -p "require('./product.json').nameLong"`
|
||||
CODE="./.build/electron/$NAME.app/Contents/MacOS/Electron"
|
||||
EXE_NAME=`node -p "require('./product.json').nameShort"`
|
||||
CODE="./.build/electron/$NAME.app/Contents/MacOS/$EXE_NAME"
|
||||
else
|
||||
NAME=`node -p "require('./product.json').applicationName"`
|
||||
CODE=".build/electron/$NAME"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// @ts-check
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const perf = require('@vscode/vscode-perf');
|
||||
|
||||
const VSCODE_FOLDER = path.join(__dirname, '..');
|
||||
@@ -62,9 +63,14 @@ function getExePath(buildPath) {
|
||||
}
|
||||
let relativeExePath;
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
relativeExePath = path.join('Contents', 'MacOS', 'Electron');
|
||||
case 'darwin': {
|
||||
const product = require(path.join(buildPath, 'Contents', 'Resources', 'app', 'product.json'));
|
||||
relativeExePath = path.join('Contents', 'MacOS', product.nameShort);
|
||||
if (!fs.existsSync(path.join(buildPath, relativeExePath))) {
|
||||
relativeExePath = path.join('Contents', 'MacOS', 'Electron');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'linux': {
|
||||
const product = require(path.join(buildPath, 'resources', 'app', 'product.json'));
|
||||
relativeExePath = product.applicationName;
|
||||
|
||||
+2
-1
@@ -18,7 +18,8 @@ function code() {
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
NAME=`node -p "require('./product.json').nameLong"`
|
||||
CODE="./.build/electron/$NAME.app/Contents/MacOS/Electron"
|
||||
EXE_NAME=`node -p "require('./product.json').nameShort"`
|
||||
CODE="./.build/electron/$NAME.app/Contents/MacOS/$EXE_NAME"
|
||||
else
|
||||
NAME=`node -p "require('./product.json').applicationName"`
|
||||
CODE=".build/electron/$NAME"
|
||||
|
||||
@@ -11,7 +11,8 @@ pushd $ROOT
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
NAME=`node -p "require('./product.json').nameLong"`
|
||||
CODE="$ROOT/.build/electron/$NAME.app/Contents/MacOS/Electron"
|
||||
EXE_NAME=`node -p "require('./product.json').nameShort"`
|
||||
CODE="$ROOT/.build/electron/$NAME.app/Contents/MacOS/$EXE_NAME"
|
||||
else
|
||||
NAME=`node -p "require('./product.json').applicationName"`
|
||||
CODE="$ROOT/.build/electron/$NAME"
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ cd $ROOT
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
NAME=`node -p "require('./product.json').nameLong"`
|
||||
CODE="./.build/electron/$NAME.app/Contents/MacOS/Electron"
|
||||
EXE_NAME=`node -p "require('./product.json').nameShort"`
|
||||
CODE="./.build/electron/$NAME.app/Contents/MacOS/$EXE_NAME"
|
||||
else
|
||||
NAME=`node -p "require('./product.json').applicationName"`
|
||||
CODE=".build/electron/$NAME"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"moduleResolution": "nodenext",
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"target": "ES2022",
|
||||
"target": "ES2024",
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"skipLibCheck": true
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": [],
|
||||
"lib": [
|
||||
"ES2022"
|
||||
"ES2024"
|
||||
],
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -351,10 +351,10 @@ export class Button extends Disposable implements IButton {
|
||||
set checked(value: boolean) {
|
||||
if (value) {
|
||||
this._element.classList.add('checked');
|
||||
this._element.setAttribute('aria-checked', 'true');
|
||||
this._element.setAttribute('aria-pressed', 'true');
|
||||
} else {
|
||||
this._element.classList.remove('checked');
|
||||
this._element.setAttribute('aria-checked', 'false');
|
||||
this._element.setAttribute('aria-pressed', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,21 +10,30 @@ import { migrateUnsupportedExtensions } from '../../../../platform/extensionMana
|
||||
import { INativeServerExtensionManagementService } from '../../../../platform/extensionManagement/node/extensionManagementService.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { IStorageService } from '../../../../platform/storage/common/storage.js';
|
||||
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
|
||||
|
||||
export class ExtensionsContributions extends Disposable {
|
||||
constructor(
|
||||
@INativeServerExtensionManagementService extensionManagementService: INativeServerExtensionManagementService,
|
||||
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
|
||||
@IExtensionStorageService extensionStorageService: IExtensionStorageService,
|
||||
@IGlobalExtensionEnablementService extensionEnablementService: IGlobalExtensionEnablementService,
|
||||
@INativeServerExtensionManagementService private readonly extensionManagementService: INativeServerExtensionManagementService,
|
||||
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
|
||||
@IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService,
|
||||
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
|
||||
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@ILogService logService: ILogService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
|
||||
extensionManagementService.cleanUp();
|
||||
migrateUnsupportedExtensions(extensionManagementService, extensionGalleryService, extensionStorageService, extensionEnablementService, logService);
|
||||
|
||||
this.migrateUnsupportedExtensions();
|
||||
ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService);
|
||||
}
|
||||
|
||||
private async migrateUnsupportedExtensions(): Promise<void> {
|
||||
for (const profile of this.userDataProfilesService.profiles) {
|
||||
await migrateUnsupportedExtensions(profile, this.extensionManagementService, this.extensionGalleryService, this.extensionStorageService, this.extensionEnablementService, this.logService);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export async function main(argv: string[]): Promise<void> {
|
||||
tunnelProcess = spawn('cargo', ['run', '--', subcommand, ...tunnelArgs], { cwd: join(getAppRoot(), 'cli'), stdio, env });
|
||||
} else {
|
||||
const appPath = process.platform === 'darwin'
|
||||
// ./Contents/MacOS/Electron => ./Contents/Resources/app/bin/code-tunnel-insiders
|
||||
// ./Contents/MacOS/Code => ./Contents/Resources/app/bin/code-tunnel-insiders
|
||||
? join(dirname(dirname(process.execPath)), 'Resources', 'app')
|
||||
: dirname(process.execPath);
|
||||
const tunnelCommand = join(appPath, 'bin', `${product.tunnelApplicationName}${isWindows ? '.exe' : ''}`);
|
||||
|
||||
@@ -427,6 +427,7 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
private readonly _dropdown: DropdownMenuActionViewItem;
|
||||
private _container: HTMLElement | null = null;
|
||||
private readonly _storageKey: string;
|
||||
private readonly _primaryActionListener = this._register(new MutableDisposable());
|
||||
|
||||
get onDidChangeDropdownVisibility(): Event<boolean> {
|
||||
return this._dropdown.onDidChangeVisibility;
|
||||
@@ -468,14 +469,18 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
|
||||
this._dropdown = this._register(new DropdownMenuActionViewItem(submenuAction, submenuAction.actions, this._contextMenuService, dropdownOptions));
|
||||
if (options?.togglePrimaryAction) {
|
||||
this._register(this._dropdown.actionRunner.onDidRun((e: IRunEvent) => {
|
||||
if (e.action instanceof MenuItemAction) {
|
||||
this.update(e.action);
|
||||
}
|
||||
}));
|
||||
this.registerTogglePrimaryActionListener();
|
||||
}
|
||||
}
|
||||
|
||||
private registerTogglePrimaryActionListener(): void {
|
||||
this._primaryActionListener.value = this._dropdown.actionRunner.onDidRun((e: IRunEvent) => {
|
||||
if (e.action instanceof MenuItemAction) {
|
||||
this.update(e.action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private update(lastAction: MenuItemAction): void {
|
||||
if (this._options?.togglePrimaryAction) {
|
||||
this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.MACHINE);
|
||||
@@ -516,6 +521,9 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
|
||||
this._defaultAction.actionRunner = actionRunner;
|
||||
this._dropdown.actionRunner = actionRunner;
|
||||
if (this._primaryActionListener.value) {
|
||||
this.registerTogglePrimaryActionListener();
|
||||
}
|
||||
}
|
||||
|
||||
override get actionRunner(): IActionRunner {
|
||||
|
||||
@@ -251,6 +251,7 @@ export class MenuId {
|
||||
static readonly ChatWelcomeContext = new MenuId('ChatWelcomeContext');
|
||||
static readonly ChatMessageFooter = new MenuId('ChatMessageFooter');
|
||||
static readonly ChatExecute = new MenuId('ChatExecute');
|
||||
static readonly ChatExecuteQueue = new MenuId('ChatExecuteQueue');
|
||||
static readonly ChatInput = new MenuId('ChatInput');
|
||||
static readonly ChatInputSide = new MenuId('ChatInputSide');
|
||||
static readonly ChatModePicker = new MenuId('ChatModePicker');
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { VSBuffer } from '../../../base/common/buffer.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
|
||||
export interface IBrowserViewBounds {
|
||||
windowId: number;
|
||||
@@ -83,10 +84,16 @@ export interface IBrowserViewFaviconChangeEvent {
|
||||
favicon: string;
|
||||
}
|
||||
|
||||
export enum BrowserNewPageLocation {
|
||||
Foreground = 'foreground',
|
||||
Background = 'background',
|
||||
NewWindow = 'newWindow'
|
||||
}
|
||||
export interface IBrowserViewNewPageRequest {
|
||||
url: string;
|
||||
name?: string;
|
||||
background: boolean;
|
||||
resource: URI;
|
||||
location: BrowserNewPageLocation;
|
||||
// Only applicable if location is NewWindow
|
||||
position?: { x?: number; y?: number; width?: number; height?: number };
|
||||
}
|
||||
|
||||
export interface IBrowserViewFindInPageOptions {
|
||||
@@ -110,6 +117,11 @@ export enum BrowserViewStorageScope {
|
||||
|
||||
export const ipcBrowserViewChannelName = 'browserView';
|
||||
|
||||
/**
|
||||
* This should match the isolated world ID defined in `preload-browserView.ts`.
|
||||
*/
|
||||
export const browserViewIsolatedWorldId = 999;
|
||||
|
||||
export interface IBrowserViewService {
|
||||
/**
|
||||
* Dynamic events that return an Event for a specific browser view ID.
|
||||
@@ -239,6 +251,14 @@ export interface IBrowserViewService {
|
||||
*/
|
||||
stopFindInPage(id: string, keepSelection?: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the currently selected text in the browser view.
|
||||
* Returns immediately with empty string if the page is still loading.
|
||||
* @param id The browser view identifier
|
||||
* @returns The selected text, or empty string if no selection or page is loading
|
||||
*/
|
||||
getSelectedText(id: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Clear all storage data for the global browser session
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
/**
|
||||
* Preload script for pages loaded in Integrated Browser
|
||||
*
|
||||
* It runs in an isolated context that Electron calls an "isolated world".
|
||||
* Specifically the isolated world with worldId 999, which shows in DevTools as "Electron Isolated Context".
|
||||
* Despite being isolated, it still runs on the same page as the JS from the actual loaded website
|
||||
* which runs on the so-called "main world" (worldId 0. In DevTools as "top").
|
||||
*
|
||||
* Learn more: see Electron docs for Security, contextBridge, and Context Isolation.
|
||||
*/
|
||||
(function () {
|
||||
|
||||
const { contextBridge } = require('electron');
|
||||
|
||||
// #######################################################################
|
||||
// ### ###
|
||||
// ### !!! DO NOT USE GET/SET PROPERTIES ANYWHERE HERE !!! ###
|
||||
// ### !!! UNLESS THE ACCESS IS WITHOUT SIDE EFFECTS !!! ###
|
||||
// ### (https://github.com/electron/electron/issues/25516) ###
|
||||
// ### ###
|
||||
// #######################################################################
|
||||
const globals = {
|
||||
/**
|
||||
* Get the currently selected text in the page.
|
||||
*/
|
||||
getSelectedText(): string {
|
||||
try {
|
||||
// Even if the page has overridden window.getSelection, our call here will still reach the original
|
||||
// implementation. That's because Electron proxies functions, such as getSelectedText here, that are
|
||||
// exposed to a different context via exposeInIsolatedWorld or exposeInMainWorld.
|
||||
return window.getSelection()?.toString() ?? '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Use `contextBridge` APIs to expose globals to the same isolated world where this preload script runs (worldId 999).
|
||||
// The globals object will be recursively frozen (and for functions also proxied) by Electron to prevent
|
||||
// modification within the given context.
|
||||
contextBridge.exposeInIsolatedWorld(999, 'browserViewAPI', globals);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}());
|
||||
@@ -4,16 +4,18 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { WebContentsView, webContents } from 'electron';
|
||||
import { FileAccess } from '../../../base/common/network.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { VSBuffer } from '../../../base/common/buffer.js';
|
||||
import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent } from '../common/browserView.js';
|
||||
import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation, browserViewIsolatedWorldId } from '../common/browserView.js';
|
||||
import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js';
|
||||
import { IWindowsMainService } from '../../windows/electron-main/windows.js';
|
||||
import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js';
|
||||
import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js';
|
||||
import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js';
|
||||
import { isMacintosh } from '../../../base/common/platform.js';
|
||||
import { BrowserViewUri } from '../common/browserViewUri.js';
|
||||
|
||||
/** Key combinations that are used in system-level shortcuts. */
|
||||
const nativeShortcuts = new Set([
|
||||
@@ -38,6 +40,7 @@ export class BrowserView extends Disposable {
|
||||
private _lastScreenshot: VSBuffer | undefined = undefined;
|
||||
private _lastFavicon: string | undefined = undefined;
|
||||
private _lastError: IBrowserViewLoadError | undefined = undefined;
|
||||
private _lastUserGestureTimestamp: number = -Infinity;
|
||||
|
||||
private _window: IBaseWindow | undefined;
|
||||
private _isSendingKeyEvent = false;
|
||||
@@ -76,40 +79,69 @@ export class BrowserView extends Disposable {
|
||||
readonly onDidClose: Event<void> = this._onDidClose.event;
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
private readonly viewSession: Electron.Session,
|
||||
private readonly storageScope: BrowserViewStorageScope,
|
||||
createChildView: (options?: Electron.WebContentsViewConstructorOptions) => BrowserView,
|
||||
options: Electron.WebContentsViewConstructorOptions | undefined,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService
|
||||
) {
|
||||
super();
|
||||
|
||||
const webPreferences: Electron.WebPreferences & { type: ReturnType<Electron.WebContents['getType']> } = {
|
||||
...options?.webPreferences,
|
||||
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
webviewTag: false,
|
||||
session: viewSession,
|
||||
preload: FileAccess.asFileUri('vs/platform/browserView/electron-browser/preload-browserView.js').fsPath,
|
||||
|
||||
// TODO@kycutler: Remove this once https://github.com/electron/electron/issues/42578 is fixed
|
||||
type: 'browserView'
|
||||
};
|
||||
|
||||
this._view = new WebContentsView({ webPreferences });
|
||||
this._view = new WebContentsView({
|
||||
webPreferences,
|
||||
// Passing an `undefined` webContents triggers an error in Electron.
|
||||
...(options?.webContents ? { webContents: options.webContents } : {})
|
||||
});
|
||||
this._view.setBackgroundColor('#FFFFFF');
|
||||
|
||||
this._view.webContents.setWindowOpenHandler((details) => {
|
||||
// For new tab requests, fire event for workbench to handle
|
||||
if (details.disposition === 'background-tab' || details.disposition === 'foreground-tab') {
|
||||
this._onDidRequestNewPage.fire({
|
||||
url: details.url,
|
||||
name: details.frameName || undefined,
|
||||
background: details.disposition === 'background-tab'
|
||||
});
|
||||
return { action: 'deny' }; // Deny the default browser behavior since we're handling it
|
||||
const location = (() => {
|
||||
switch (details.disposition) {
|
||||
case 'background-tab': return BrowserNewPageLocation.Background;
|
||||
case 'foreground-tab': return BrowserNewPageLocation.Foreground;
|
||||
case 'new-window': return BrowserNewPageLocation.NewWindow;
|
||||
default: return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!location || !this.consumePopupPermission(location)) {
|
||||
// Eventually we may want to surface this. For now, just silently block it.
|
||||
return { action: 'deny' };
|
||||
}
|
||||
|
||||
// Deny other requests like new windows.
|
||||
return { action: 'deny' };
|
||||
return {
|
||||
action: 'allow',
|
||||
createWindow: (options) => {
|
||||
const childView = createChildView(options);
|
||||
const resource = BrowserViewUri.forUrl(details.url, childView.id);
|
||||
|
||||
// Fire event for the workbench to open this view
|
||||
this._onDidRequestNewPage.fire({
|
||||
resource,
|
||||
location,
|
||||
position: { x: options.x, y: options.y, width: options.width, height: options.height }
|
||||
});
|
||||
|
||||
// Return the webContents so Electron can complete the window.open() call
|
||||
return childView.webContents;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this._view.webContents.on('destroyed', () => {
|
||||
@@ -250,6 +282,20 @@ export class BrowserView extends Disposable {
|
||||
}
|
||||
});
|
||||
|
||||
// Track user gestures for popup blocking logic.
|
||||
// Roughly based on https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation.
|
||||
webContents.on('input-event', (_event, input) => {
|
||||
switch (input.type) {
|
||||
case 'rawKeyDown':
|
||||
case 'keyDown':
|
||||
case 'mouseDown':
|
||||
case 'pointerDown':
|
||||
case 'pointerUp':
|
||||
case 'touchEnd':
|
||||
this._lastUserGestureTimestamp = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
// For now, always prevent sites from blocking unload.
|
||||
// In the future we may want to show a dialog to ask the user,
|
||||
// with heavy restrictions regarding interaction and repeated prompts.
|
||||
@@ -268,6 +314,22 @@ export class BrowserView extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
private consumePopupPermission(location: BrowserNewPageLocation): boolean {
|
||||
switch (location) {
|
||||
case BrowserNewPageLocation.Foreground:
|
||||
case BrowserNewPageLocation.Background:
|
||||
return true;
|
||||
case BrowserNewPageLocation.NewWindow:
|
||||
// Each user gesture allows one popup window within 1 second
|
||||
if (this._lastUserGestureTimestamp > Date.now() - 1000) {
|
||||
this._lastUserGestureTimestamp = -Infinity;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get webContents(): Electron.WebContents {
|
||||
return this._view.webContents;
|
||||
}
|
||||
@@ -475,6 +537,23 @@ export class BrowserView extends Disposable {
|
||||
this._view.webContents.stopFindInPage(keepSelection ? 'keepSelection' : 'clearSelection');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected text in the browser view.
|
||||
* Returns immediately with empty string if the page is still loading.
|
||||
*/
|
||||
async getSelectedText(): Promise<string> {
|
||||
// we don't want to wait for the page to finish loading, which executeJavaScript normally does.
|
||||
if (this._view.webContents.isLoading()) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
// Uses our preloaded contextBridge-exposed API.
|
||||
return await this._view.webContents.executeJavaScriptInIsolatedWorld(browserViewIsolatedWorldId, [{ code: 'window.browserViewAPI?.getSelectedText?.() ?? ""' }]);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all storage data for this browser view's session
|
||||
*/
|
||||
|
||||
@@ -78,6 +78,27 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a child browser view (used by window.open handler)
|
||||
*/
|
||||
private createBrowserView(id: string, session: Electron.Session, scope: BrowserViewStorageScope, options?: Electron.WebContentsViewConstructorOptions): BrowserView {
|
||||
if (this.browserViews.has(id)) {
|
||||
throw new Error(`Browser view with id ${id} already exists`);
|
||||
}
|
||||
|
||||
const view = this.instantiationService.createInstance(
|
||||
BrowserView,
|
||||
id,
|
||||
session,
|
||||
scope,
|
||||
// Recursive factory for nested windows
|
||||
(options) => this.createBrowserView(generateUuid(), session, scope, options),
|
||||
options
|
||||
);
|
||||
this.browserViews.set(id, view);
|
||||
return view;
|
||||
}
|
||||
|
||||
async getOrCreateBrowserView(id: string, scope: BrowserViewStorageScope, workspaceId?: string): Promise<IBrowserViewState> {
|
||||
if (this.browserViews.has(id)) {
|
||||
// Note: scope will be ignored if the view already exists.
|
||||
@@ -90,8 +111,7 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
|
||||
this.configureSession(session);
|
||||
BrowserViewMainService.knownSessions.add(session);
|
||||
|
||||
const view = this.instantiationService.createInstance(BrowserView, session, resolvedScope);
|
||||
this.browserViews.set(id, view);
|
||||
const view = this.createBrowserView(id, session, resolvedScope);
|
||||
|
||||
return view.getState();
|
||||
}
|
||||
@@ -223,6 +243,10 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
|
||||
return this._getBrowserView(id).stopFindInPage(keepSelection);
|
||||
}
|
||||
|
||||
async getSelectedText(id: string): Promise<string> {
|
||||
return this._getBrowserView(id).getSelectedText();
|
||||
}
|
||||
|
||||
async clearStorage(id: string): Promise<void> {
|
||||
return this._getBrowserView(id).clearStorage();
|
||||
}
|
||||
|
||||
@@ -939,6 +939,9 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio
|
||||
if (checked.indexOf(extension) !== -1) {
|
||||
return [];
|
||||
}
|
||||
if (areSameExtensions(extension.identifier, { id: this.productService.defaultChatAgent.extensionId })) {
|
||||
return [];
|
||||
}
|
||||
checked.push(extension);
|
||||
const extensionsPack = extension.manifest.extensionPack ? extension.manifest.extensionPack : [];
|
||||
if (extensionsPack.length) {
|
||||
|
||||
@@ -1157,8 +1157,23 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle
|
||||
|
||||
const runQuery = async (query: Query, token: CancellationToken) => {
|
||||
const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, extensionGalleryManifest, token);
|
||||
extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source));
|
||||
return { extensions, total };
|
||||
|
||||
const result: IGalleryExtension[] = [];
|
||||
let defaultChatAgentExtension: IGalleryExtension | undefined;
|
||||
for (let index = 0; index < extensions.length; index++) {
|
||||
const extension = extensions[index];
|
||||
setTelemetry(extension, ((query.pageNumber - 1) * query.pageSize) + index, options.source);
|
||||
if (areSameExtensions(extension.identifier, { id: this.productService.defaultChatAgent.extensionId, })) {
|
||||
defaultChatAgentExtension = extension;
|
||||
} else {
|
||||
result.push(extension);
|
||||
}
|
||||
}
|
||||
if (defaultChatAgentExtension) {
|
||||
result.push(defaultChatAgentExtension);
|
||||
}
|
||||
|
||||
return { extensions: result, total };
|
||||
};
|
||||
const { extensions, total } = await runQuery(query, token);
|
||||
const getPage = async (pageIndex: number, ct: CancellationToken) => {
|
||||
@@ -1976,6 +1991,16 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle
|
||||
}
|
||||
}
|
||||
|
||||
deprecated[this.productService.defaultChatAgent.extensionId.toLowerCase()] = {
|
||||
disallowInstall: true,
|
||||
extension: {
|
||||
id: this.productService.defaultChatAgent.chatExtensionId,
|
||||
displayName: 'GitHub Copilot Chat',
|
||||
autoMigrate: { storage: false, donotDisable: true },
|
||||
preRelease: this.productService.quality !== 'stable'
|
||||
}
|
||||
};
|
||||
|
||||
return { malicious, deprecated, search, autoUpdate };
|
||||
}
|
||||
|
||||
|
||||
@@ -339,7 +339,10 @@ export interface IDeprecationInfo {
|
||||
readonly extension?: {
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly autoMigrate?: { readonly storage: boolean };
|
||||
readonly autoMigrate?: {
|
||||
readonly storage: boolean;
|
||||
readonly donotDisable?: boolean;
|
||||
};
|
||||
readonly preRelease?: boolean;
|
||||
};
|
||||
readonly settings?: readonly string[];
|
||||
|
||||
@@ -10,6 +10,7 @@ import { IExtensionStorageService } from './extensionStorage.js';
|
||||
import { ExtensionType } from '../../extensions/common/extensions.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import * as semver from '../../../base/common/semver/semver.js';
|
||||
import { IUserDataProfile } from '../../userDataProfile/common/userDataProfile.js';
|
||||
|
||||
/**
|
||||
* Migrates the installed unsupported nightly extension to a supported pre-release extension. It includes following:
|
||||
@@ -18,13 +19,13 @@ import * as semver from '../../../base/common/semver/semver.js';
|
||||
* - the extension is not installed
|
||||
* - or it is a release version and the unsupported extension is enabled.
|
||||
*/
|
||||
export async function migrateUnsupportedExtensions(extensionManagementService: IExtensionManagementService, galleryService: IExtensionGalleryService, extensionStorageService: IExtensionStorageService, extensionEnablementService: IGlobalExtensionEnablementService, logService: ILogService): Promise<void> {
|
||||
export async function migrateUnsupportedExtensions(profile: IUserDataProfile | undefined, extensionManagementService: IExtensionManagementService, galleryService: IExtensionGalleryService, extensionStorageService: IExtensionStorageService, extensionEnablementService: IGlobalExtensionEnablementService, logService: ILogService): Promise<void> {
|
||||
try {
|
||||
const extensionsControlManifest = await extensionManagementService.getExtensionsControlManifest();
|
||||
if (!extensionsControlManifest.deprecated) {
|
||||
return;
|
||||
}
|
||||
const installed = await extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const installed = await extensionManagementService.getInstalled(ExtensionType.User, profile?.extensionsResource);
|
||||
for (const [unsupportedExtensionId, deprecated] of Object.entries(extensionsControlManifest.deprecated)) {
|
||||
if (!deprecated?.extension) {
|
||||
continue;
|
||||
@@ -49,14 +50,14 @@ export async function migrateUnsupportedExtensions(extensionManagementService: I
|
||||
logService.info(`Migrating '${unsupportedExtension.identifier.id}' extension to '${preReleaseExtensionId}' extension...`);
|
||||
|
||||
const isUnsupportedExtensionEnabled = !extensionEnablementService.getDisabledExtensions().some(e => areSameExtensions(e, unsupportedExtension.identifier));
|
||||
await extensionManagementService.uninstall(unsupportedExtension);
|
||||
await extensionManagementService.uninstall(unsupportedExtension, { profileLocation: profile?.extensionsResource });
|
||||
logService.info(`Uninstalled the unsupported extension '${unsupportedExtension.identifier.id}'`);
|
||||
|
||||
let preReleaseExtension = installed.find(i => areSameExtensions(i.identifier, { id: preReleaseExtensionId }));
|
||||
if (!preReleaseExtension || (!preReleaseExtension.isPreReleaseVersion && isUnsupportedExtensionEnabled)) {
|
||||
preReleaseExtension = await extensionManagementService.installFromGallery(gallery, { installPreReleaseVersion: true, isMachineScoped: unsupportedExtension.isMachineScoped, operation: InstallOperation.Migrate, context: { [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } });
|
||||
if (!preReleaseExtension || (preReleaseExtension.isPreReleaseVersion !== !!preRelease && isUnsupportedExtensionEnabled)) {
|
||||
preReleaseExtension = await extensionManagementService.installFromGallery(gallery, { installPreReleaseVersion: preRelease, isMachineScoped: unsupportedExtension.isMachineScoped, operation: InstallOperation.Migrate, profileLocation: profile?.extensionsResource, context: { [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } });
|
||||
logService.info(`Installed the pre-release extension '${preReleaseExtension.identifier.id}'`);
|
||||
if (!isUnsupportedExtensionEnabled) {
|
||||
if (!autoMigrate.donotDisable && !isUnsupportedExtensionEnabled) {
|
||||
await extensionEnablementService.disableExtension(preReleaseExtension.identifier);
|
||||
logService.info(`Disabled the pre-release extension '${preReleaseExtension.identifier.id}' because the unsupported extension '${unsupportedExtension.identifier.id}' is disabled`);
|
||||
}
|
||||
@@ -85,7 +86,7 @@ export async function migrateUnsupportedExtensions(extensionManagementService: I
|
||||
continue;
|
||||
}
|
||||
|
||||
await extensionManagementService.installFromGallery(gallery, { installPreReleaseVersion: extensionToAutoUpdate.preRelease, isMachineScoped: extensionToAutoUpdate.isMachineScoped, operation: InstallOperation.Update, context: { [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } });
|
||||
await extensionManagementService.installFromGallery(gallery, { installPreReleaseVersion: extensionToAutoUpdate.preRelease, isMachineScoped: extensionToAutoUpdate.isMachineScoped, operation: InstallOperation.Update, profileLocation: profile?.extensionsResource, context: { [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } });
|
||||
logService.info(`Autoupdated '${extensionToAutoUpdate.identifier.id}' extension to '${gallery.version}' extension.`);
|
||||
} catch (error) {
|
||||
logService.error(error);
|
||||
|
||||
@@ -43,6 +43,10 @@ const _allApiProposals = {
|
||||
chatContextProvider: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts',
|
||||
},
|
||||
chatHooks: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatHooks.d.ts',
|
||||
version: 1
|
||||
},
|
||||
chatOutputRenderer: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatOutputRenderer.d.ts',
|
||||
},
|
||||
|
||||
@@ -77,6 +77,18 @@ configurationRegistry.registerConfiguration({
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
description: localize('showReleaseNotes', "Show Release Notes after an update. The Release Notes are fetched from a Microsoft online service."),
|
||||
tags: ['usesOnlineServices']
|
||||
},
|
||||
'update.statusBar': {
|
||||
type: 'string',
|
||||
enum: ['hidden', 'actionable', 'detailed'],
|
||||
default: 'detailed',
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
description: localize('statusBar', "Controls the visibility of the update status bar entry."),
|
||||
enumDescriptions: [
|
||||
localize('hidden', "The status bar entry is never shown."),
|
||||
localize('actionable', "The status bar entry is shown when an action is required (e.g., download, install, or restart)."),
|
||||
localize('detailed', "The status bar entry is shown for all update states including progress.")
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,11 +69,11 @@ export type Disabled = { type: StateType.Disabled; reason: DisablementReason };
|
||||
export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string };
|
||||
export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit: boolean };
|
||||
export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate };
|
||||
export type Downloading = { type: StateType.Downloading; explicit: boolean; overwrite: boolean };
|
||||
export type Downloading = { type: StateType.Downloading; update?: IUpdate; explicit: boolean; overwrite: boolean; downloadedBytes?: number; totalBytes?: number; startTime?: number };
|
||||
export type Downloaded = { type: StateType.Downloaded; update: IUpdate; explicit: boolean; overwrite: boolean };
|
||||
export type Updating = { type: StateType.Updating; update: IUpdate };
|
||||
export type Ready = { type: StateType.Ready; update: IUpdate; explicit: boolean; overwrite: boolean };
|
||||
export type Overwriting = { type: StateType.Overwriting; explicit: boolean };
|
||||
export type Overwriting = { type: StateType.Overwriting; update: IUpdate; explicit: boolean };
|
||||
|
||||
export type State = Uninitialized | Disabled | Idle | CheckingForUpdates | AvailableForDownload | Downloading | Downloaded | Updating | Ready | Overwriting;
|
||||
|
||||
@@ -83,11 +83,11 @@ export const State = {
|
||||
Idle: (updateType: UpdateType, error?: string): Idle => ({ type: StateType.Idle, updateType, error }),
|
||||
CheckingForUpdates: (explicit: boolean): CheckingForUpdates => ({ type: StateType.CheckingForUpdates, explicit }),
|
||||
AvailableForDownload: (update: IUpdate): AvailableForDownload => ({ type: StateType.AvailableForDownload, update }),
|
||||
Downloading: (explicit: boolean, overwrite: boolean): Downloading => ({ type: StateType.Downloading, explicit, overwrite }),
|
||||
Downloading: (update: IUpdate | undefined, explicit: boolean, overwrite: boolean, downloadedBytes?: number, totalBytes?: number, startTime?: number): Downloading => ({ type: StateType.Downloading, update, explicit, overwrite, downloadedBytes, totalBytes, startTime }),
|
||||
Downloaded: (update: IUpdate, explicit: boolean, overwrite: boolean): Downloaded => ({ type: StateType.Downloaded, update, explicit, overwrite }),
|
||||
Updating: (update: IUpdate): Updating => ({ type: StateType.Updating, update }),
|
||||
Ready: (update: IUpdate, explicit: boolean, overwrite: boolean): Ready => ({ type: StateType.Ready, update, explicit, overwrite }),
|
||||
Overwriting: (explicit: boolean): Overwriting => ({ type: StateType.Overwriting, explicit }),
|
||||
Overwriting: (update: IUpdate, explicit: boolean): Overwriting => ({ type: StateType.Overwriting, update, explicit }),
|
||||
};
|
||||
|
||||
export interface IAutoUpdater extends Event.NodeEventEmitter {
|
||||
|
||||
@@ -248,7 +248,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
this.logService.info('update#readyStateCheck: newer update available, restarting update machinery');
|
||||
await this.cancelPendingUpdate();
|
||||
this._overwrite = true;
|
||||
this.setState(State.Overwriting(explicit));
|
||||
this.setState(State.Overwriting(this._state.update, explicit));
|
||||
this.doCheckForUpdates(explicit, pendingUpdateCommit);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(State.Downloading(this.state.explicit, this._overwrite));
|
||||
this.setState(State.Downloading(this.state.type === StateType.Overwriting ? this.state.update : undefined, this.state.explicit, this._overwrite));
|
||||
}
|
||||
|
||||
private onUpdateDownloaded(update: IUpdate): void {
|
||||
|
||||
@@ -8,11 +8,13 @@ import { existsSync, unlinkSync } from 'fs';
|
||||
import { mkdir, readFile, unlink } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { app } from 'electron';
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
import { Delayer, timeout } from '../../../base/common/async.js';
|
||||
import { VSBuffer } from '../../../base/common/buffer.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { memoize } from '../../../base/common/decorators.js';
|
||||
import { hash } from '../../../base/common/hash.js';
|
||||
import * as path from '../../../base/common/path.js';
|
||||
import { transform } from '../../../base/common/stream.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { checksum } from '../../../base/node/crypto.js';
|
||||
import * as pfs from '../../../base/node/pfs.js';
|
||||
@@ -188,7 +190,8 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
this.setState(State.Downloading(explicit, this._overwrite));
|
||||
const startTime = Date.now();
|
||||
this.setState(State.Downloading(update, explicit, this._overwrite, 0, undefined, startTime));
|
||||
|
||||
return this.cleanup(update.version).then(() => {
|
||||
return this.getUpdatePackagePath(update.version).then(updatePackagePath => {
|
||||
@@ -200,7 +203,32 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
const downloadPath = `${updatePackagePath}.tmp`;
|
||||
|
||||
return this.requestService.request({ url: update.url }, CancellationToken.None)
|
||||
.then(context => this.fileService.writeFile(URI.file(downloadPath), context.stream))
|
||||
.then(context => {
|
||||
// Get total size from Content-Length header
|
||||
const contentLengthHeader = context.res.headers['content-length'];
|
||||
const contentLength = typeof contentLengthHeader === 'string' ? contentLengthHeader : undefined;
|
||||
const totalBytes = contentLength ? parseInt(contentLength, 10) : undefined;
|
||||
|
||||
// Track downloaded bytes and update state periodically using Delayer
|
||||
let downloadedBytes = 0;
|
||||
const progressDelayer = new Delayer<void>(500);
|
||||
const progressStream = transform<VSBuffer, VSBuffer>(
|
||||
context.stream,
|
||||
{
|
||||
data: data => {
|
||||
downloadedBytes += data.byteLength;
|
||||
progressDelayer.trigger(() => {
|
||||
this.setState(State.Downloading(update, explicit, this._overwrite, downloadedBytes, totalBytes, startTime));
|
||||
});
|
||||
return data;
|
||||
}
|
||||
},
|
||||
chunks => VSBuffer.concat(chunks)
|
||||
);
|
||||
|
||||
return this.fileService.writeFile(URI.file(downloadPath), progressStream)
|
||||
.finally(() => progressDelayer.dispose());
|
||||
})
|
||||
.then(update.sha256hash ? () => checksum(downloadPath, update.sha256hash) : () => undefined)
|
||||
.then(() => pfs.Promises.rename(downloadPath, updatePackagePath, false /* no retry */))
|
||||
.then(() => updatePackagePath);
|
||||
@@ -326,7 +354,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
|
||||
const update: IUpdate = { version: 'unknown', productVersion: 'unknown' };
|
||||
|
||||
this.setState(State.Downloading(true, false));
|
||||
this.setState(State.Downloading(update, true, false));
|
||||
this.availableUpdate = { packagePath };
|
||||
this.setState(State.Downloaded(update, true, false));
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ import './mainThreadChatStatus.js';
|
||||
import './mainThreadChatOutputRenderer.js';
|
||||
import './mainThreadChatSessions.js';
|
||||
import './mainThreadDataChannels.js';
|
||||
import './mainThreadHooks.js';
|
||||
|
||||
export class ExtensionPoints implements IWorkbenchContribution {
|
||||
|
||||
|
||||
@@ -419,61 +419,66 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
|
||||
}
|
||||
|
||||
const originalEditor = this._editorService.editors.find(editor => editor.resource?.toString() === originalResource.toString());
|
||||
const originalModel = this._chatService.getSession(originalResource);
|
||||
const originalModel = this._chatService.getActiveSessionReference(originalResource);
|
||||
const contribution = this._chatSessionsService.getAllChatSessionContributions().find(c => c.type === chatSessionType);
|
||||
|
||||
// Migrate todos from old session to new session
|
||||
this._chatTodoListService.migrateTodos(originalResource, modifiedResource);
|
||||
try {
|
||||
|
||||
// Find the group containing the original editor
|
||||
const originalGroup =
|
||||
this.editorGroupService.groups.find(group => group.editors.some(editor => isEqual(editor.resource, originalResource)))
|
||||
?? this.editorGroupService.activeGroup;
|
||||
// Migrate todos from old session to new session
|
||||
this._chatTodoListService.migrateTodos(originalResource, modifiedResource);
|
||||
|
||||
const options: IChatEditorOptions = {
|
||||
title: {
|
||||
preferred: originalEditor?.getName() || undefined,
|
||||
fallback: localize('chatEditorContributionName', "{0}", contribution?.displayName),
|
||||
}
|
||||
};
|
||||
// Find the group containing the original editor
|
||||
const originalGroup =
|
||||
this.editorGroupService.groups.find(group => group.editors.some(editor => isEqual(editor.resource, originalResource)))
|
||||
?? this.editorGroupService.activeGroup;
|
||||
|
||||
// Prefetch the chat session content to make the subsequent editor swap quick
|
||||
const newSession = await this._chatSessionsService.getOrCreateChatSession(
|
||||
URI.revive(modifiedResource),
|
||||
CancellationToken.None,
|
||||
);
|
||||
|
||||
if (originalEditor) {
|
||||
newSession.transferredState = originalEditor instanceof ChatEditorInput
|
||||
? { editingSession: originalEditor.transferOutEditingSession(), inputState: originalModel?.inputModel.toJSON() }
|
||||
: undefined;
|
||||
|
||||
this._editorService.replaceEditors([{
|
||||
editor: originalEditor,
|
||||
replacement: {
|
||||
resource: modifiedResource,
|
||||
options,
|
||||
},
|
||||
}], originalGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
// If chat editor is in the side panel, then those are not listed as editors.
|
||||
// In that case we need to transfer editing session using the original model.
|
||||
if (originalModel) {
|
||||
newSession.transferredState = {
|
||||
editingSession: originalModel.editingSession,
|
||||
inputState: originalModel.inputModel.toJSON()
|
||||
const options: IChatEditorOptions = {
|
||||
title: {
|
||||
preferred: originalEditor?.getName() || undefined,
|
||||
fallback: localize('chatEditorContributionName', "{0}", contribution?.displayName),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const chatViewWidget = this._chatWidgetService.getWidgetBySessionResource(originalResource);
|
||||
if (chatViewWidget && isIChatViewViewContext(chatViewWidget.viewContext)) {
|
||||
await this._chatWidgetService.openSession(modifiedResource, undefined, { preserveFocus: true });
|
||||
} else {
|
||||
// Loading the session to ensure the session is created and editing session is transferred.
|
||||
const ref = await this._chatService.loadSessionForResource(modifiedResource, ChatAgentLocation.Chat, CancellationToken.None);
|
||||
ref?.dispose();
|
||||
// Prefetch the chat session content to make the subsequent editor swap quick
|
||||
const newSession = await this._chatSessionsService.getOrCreateChatSession(
|
||||
URI.revive(modifiedResource),
|
||||
CancellationToken.None,
|
||||
);
|
||||
|
||||
if (originalEditor) {
|
||||
newSession.transferredState = originalEditor instanceof ChatEditorInput
|
||||
? { editingSession: originalEditor.transferOutEditingSession(), inputState: originalModel?.object?.inputModel.toJSON() }
|
||||
: undefined;
|
||||
|
||||
await this._editorService.replaceEditors([{
|
||||
editor: originalEditor,
|
||||
replacement: {
|
||||
resource: modifiedResource,
|
||||
options,
|
||||
},
|
||||
}], originalGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
// If chat editor is in the side panel, then those are not listed as editors.
|
||||
// In that case we need to transfer editing session using the original model.
|
||||
if (originalModel) {
|
||||
newSession.transferredState = {
|
||||
editingSession: originalModel.object.editingSession,
|
||||
inputState: originalModel.object.inputModel.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
const chatViewWidget = this._chatWidgetService.getWidgetBySessionResource(originalResource);
|
||||
if (chatViewWidget && isIChatViewViewContext(chatViewWidget.viewContext)) {
|
||||
await this._chatWidgetService.openSession(modifiedResource, undefined, { preserveFocus: true });
|
||||
} else {
|
||||
// Loading the session to ensure the session is created and editing session is transferred.
|
||||
const ref = await this._chatService.loadSessionForResource(modifiedResource, ChatAgentLocation.Chat, CancellationToken.None);
|
||||
ref?.dispose();
|
||||
}
|
||||
} finally {
|
||||
originalModel?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI, UriComponents } from '../../../base/common/uri.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
|
||||
import { ExtHostContext, MainContext, MainThreadHooksShape } from '../common/extHost.protocol.js';
|
||||
import { HookResultKind, IHookResult, IHooksExecutionProxy, IHooksExecutionService } from '../../contrib/chat/common/hooksExecutionService.js';
|
||||
import { HookTypeValue, IHookCommand } from '../../contrib/chat/common/promptSyntax/hookSchema.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadHooks)
|
||||
export class MainThreadHooks extends Disposable implements MainThreadHooksShape {
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IHooksExecutionService private readonly _hooksExecutionService: IHooksExecutionService,
|
||||
) {
|
||||
super();
|
||||
const extHostProxy = extHostContext.getProxy(ExtHostContext.ExtHostHooks);
|
||||
|
||||
const proxy: IHooksExecutionProxy = {
|
||||
runHookCommand: async (hookCommand: IHookCommand, input: unknown, token: CancellationToken): Promise<IHookResult> => {
|
||||
const result = await extHostProxy.$runHookCommand(hookCommand, input, token);
|
||||
return {
|
||||
kind: result.kind as HookResultKind,
|
||||
result: result.result
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this._hooksExecutionService.setProxy(proxy);
|
||||
}
|
||||
|
||||
async $executeHook(hookType: string, sessionResource: UriComponents, input: unknown, token: CancellationToken): Promise<IHookResult[]> {
|
||||
const uri = URI.revive(sessionResource);
|
||||
return this._hooksExecutionService.executeHook(hookType as HookTypeValue, uri, { input, token });
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ import { IExtHostConsumerFileSystem } from './extHostFileSystemConsumer.js';
|
||||
import { ExtHostFileSystemEventService, FileSystemWatcherCreateOptions } from './extHostFileSystemEventService.js';
|
||||
import { IExtHostFileSystemInfo } from './extHostFileSystemInfo.js';
|
||||
import { IExtHostInitDataService } from './extHostInitDataService.js';
|
||||
import { IExtHostHooks } from './extHostHooks.js';
|
||||
import { ExtHostInteractive } from './extHostInteractive.js';
|
||||
import { ExtHostLabelService } from './extHostLabelService.js';
|
||||
import { ExtHostLanguageFeatures } from './extHostLanguageFeatures.js';
|
||||
@@ -238,6 +239,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol));
|
||||
|
||||
rpcProtocol.set(ExtHostContext.ExtHostMcp, accessor.get(IExtHostMpcService));
|
||||
rpcProtocol.set(ExtHostContext.ExtHostHooks, accessor.get(IExtHostHooks));
|
||||
|
||||
// Check that no named customers are missing
|
||||
const expected = Object.values<ProxyIdentifier<any>>(ExtHostContext);
|
||||
@@ -249,6 +251,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
const extHostMessageService = new ExtHostMessageService(rpcProtocol, extHostLogService);
|
||||
const extHostDialogs = new ExtHostDialogs(rpcProtocol);
|
||||
const extHostChatStatus = new ExtHostChatStatus(rpcProtocol);
|
||||
const extHostHooks = accessor.get(IExtHostHooks);
|
||||
|
||||
// Register API-ish commands
|
||||
ExtHostApiCommands.register(extHostCommands);
|
||||
@@ -1591,6 +1594,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
checkProposedApiEnabled(extension, 'chatPromptFiles');
|
||||
return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.skill, provider);
|
||||
},
|
||||
async executeHook(hookType: vscode.ChatHookType, options: vscode.ChatHookExecutionOptions, token?: vscode.CancellationToken): Promise<vscode.ChatHookResult[]> {
|
||||
checkProposedApiEnabled(extension, 'chatHooks');
|
||||
return extHostHooks.executeHook(hookType, options, token);
|
||||
},
|
||||
};
|
||||
|
||||
// namespace: lm
|
||||
@@ -2013,7 +2020,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
McpToolAvailability: extHostTypes.McpToolAvailability,
|
||||
McpToolInvocationContentData: extHostTypes.McpToolInvocationContentData,
|
||||
SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind,
|
||||
ChatTodoStatus: extHostTypes.ChatTodoStatus
|
||||
ChatHookResultKind: extHostTypes.ChatHookResultKind,
|
||||
ChatTodoStatus: extHostTypes.ChatTodoStatus,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +99,8 @@ import { IExtHostDocumentSaveDelegate } from './extHostDocumentData.js';
|
||||
import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js';
|
||||
import * as tasks from './shared/tasks.js';
|
||||
import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js';
|
||||
import { IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
|
||||
import { IHookCommand } from '../../contrib/chat/common/promptSyntax/hookSchema.js';
|
||||
|
||||
export type IconPathDto =
|
||||
| UriComponents
|
||||
@@ -3197,7 +3199,11 @@ export interface IStartMcpOptions {
|
||||
errorOnUserInteraction?: boolean;
|
||||
}
|
||||
|
||||
export type IHookCommandDto = Dto<IHookCommand>;
|
||||
|
||||
export interface ExtHostHooksShape {
|
||||
$runHookCommand(hookCommand: IHookCommandDto, input: unknown, token: CancellationToken): Promise<IHookResult>;
|
||||
}
|
||||
|
||||
export interface ExtHostMcpShape {
|
||||
$substituteVariables(workspaceFolder: UriComponents | undefined, value: McpServerLaunch.Serialized): Promise<McpServerLaunch.Serialized>;
|
||||
@@ -3252,6 +3258,10 @@ export interface MainThreadMcpShape {
|
||||
export interface MainThreadDataChannelsShape extends IDisposable {
|
||||
}
|
||||
|
||||
export interface MainThreadHooksShape extends IDisposable {
|
||||
$executeHook(hookType: string, sessionResource: UriComponents, input: unknown, token: CancellationToken): Promise<IHookResult[]>;
|
||||
}
|
||||
|
||||
export interface ExtHostDataChannelsShape {
|
||||
$onDidReceiveData(channelId: string, data: unknown): void;
|
||||
}
|
||||
@@ -3485,6 +3495,7 @@ export const MainContext = {
|
||||
MainThreadChatStatus: createProxyIdentifier<MainThreadChatStatusShape>('MainThreadChatStatus'),
|
||||
MainThreadAiSettingsSearch: createProxyIdentifier<MainThreadAiSettingsSearchShape>('MainThreadAiSettingsSearch'),
|
||||
MainThreadDataChannels: createProxyIdentifier<MainThreadDataChannelsShape>('MainThreadDataChannels'),
|
||||
MainThreadHooks: createProxyIdentifier<MainThreadHooksShape>('MainThreadHooks'),
|
||||
MainThreadChatSessions: createProxyIdentifier<MainThreadChatSessionsShape>('MainThreadChatSessions'),
|
||||
MainThreadChatOutputRenderer: createProxyIdentifier<MainThreadChatOutputRendererShape>('MainThreadChatOutputRenderer'),
|
||||
MainThreadChatContext: createProxyIdentifier<MainThreadChatContextShape>('MainThreadChatContext'),
|
||||
@@ -3562,6 +3573,7 @@ export const ExtHostContext = {
|
||||
ExtHostTelemetry: createProxyIdentifier<ExtHostTelemetryShape>('ExtHostTelemetry'),
|
||||
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization'),
|
||||
ExtHostMcp: createProxyIdentifier<ExtHostMcpShape>('ExtHostMcp'),
|
||||
ExtHostHooks: createProxyIdentifier<ExtHostHooksShape>('ExtHostHooks'),
|
||||
ExtHostDataChannels: createProxyIdentifier<ExtHostDataChannelsShape>('ExtHostDataChannels'),
|
||||
ExtHostChatSessions: createProxyIdentifier<ExtHostChatSessionsShape>('ExtHostChatSessions'),
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ILogService } from '../../../platform/log/common/log.js';
|
||||
import { isChatViewTitleActionContext } from '../../contrib/chat/common/actions/chatActions.js';
|
||||
import { IChatAgentRequest, IChatAgentResult, IChatAgentResultTimings, UserSelectedTools } from '../../contrib/chat/common/participants/chatAgents.js';
|
||||
import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService/chatService.js';
|
||||
import { IChatRequestHooks } from '../../contrib/chat/common/promptSyntax/hookSchema.js';
|
||||
import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js';
|
||||
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
|
||||
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
|
||||
@@ -447,6 +448,7 @@ interface InFlightChatRequest {
|
||||
requestId: string;
|
||||
extRequest: vscode.ChatRequest;
|
||||
extension: IRelaxedExtensionDescription;
|
||||
hooks?: IChatRequestHooks;
|
||||
}
|
||||
|
||||
export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsShape2 {
|
||||
@@ -623,7 +625,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
|
||||
return detector.provider.provideParticipantDetection(
|
||||
extRequest,
|
||||
{ history },
|
||||
{ history, yieldRequested: false },
|
||||
{ participants: options.participants, location: typeConvert.ChatLocation.to(options.location) },
|
||||
token
|
||||
);
|
||||
@@ -715,7 +717,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
agent.extension,
|
||||
this._logService
|
||||
);
|
||||
inFlightRequest = { requestId: requestDto.requestId, extRequest, extension: agent.extension };
|
||||
inFlightRequest = { requestId: requestDto.requestId, extRequest, extension: agent.extension, hooks: request.hooks };
|
||||
this._inFlightRequests.add(inFlightRequest);
|
||||
|
||||
|
||||
@@ -731,7 +733,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
};
|
||||
}
|
||||
|
||||
const chatContext: vscode.ChatContext = { history, chatSessionContext };
|
||||
const chatContext: vscode.ChatContext = { history, chatSessionContext, yieldRequested: request.yieldRequested ?? false };
|
||||
const task = agent.invoke(
|
||||
extRequest,
|
||||
chatContext,
|
||||
@@ -865,7 +867,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
const convertedHistory = await this.prepareHistoryTurns(agent.extension, agent.id, context);
|
||||
|
||||
const ehResult = typeConvert.ChatAgentResult.to(result);
|
||||
return (await agent.provideFollowups(ehResult, { history: convertedHistory }, token))
|
||||
return (await agent.provideFollowups(ehResult, { history: convertedHistory, yieldRequested: false }, token))
|
||||
.filter(f => {
|
||||
// The followup must refer to a participant that exists from the same extension
|
||||
const isValid = !f.participant || Iterable.some(
|
||||
@@ -965,7 +967,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
}
|
||||
|
||||
const history = await this.prepareHistoryTurns(agent.extension, agent.id, { history: context });
|
||||
return await agent.provideTitle({ history }, token);
|
||||
return await agent.provideTitle({ history, yieldRequested: false }, token);
|
||||
}
|
||||
|
||||
async $provideChatSummary(handle: number, context: IChatAgentHistoryEntryDto[], token: CancellationToken): Promise<string | undefined> {
|
||||
@@ -975,7 +977,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
}
|
||||
|
||||
const history = await this.prepareHistoryTurns(agent.extension, agent.id, { history: context });
|
||||
return await agent.provideSummary({ history }, token);
|
||||
return await agent.provideSummary({ history, yieldRequested: false }, token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -661,7 +661,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
|
||||
const chatRequest = typeConvert.ChatAgentRequest.to(request, undefined, await this.getModelForRequest(request, entry.sessionObj.extension), [], new Map(), entry.sessionObj.extension, this._logService);
|
||||
|
||||
const stream = entry.sessionObj.getActiveRequestStream(request);
|
||||
await entry.sessionObj.session.requestHandler(chatRequest, { history: history }, stream.apiObject, token);
|
||||
await entry.sessionObj.session.requestHandler(chatRequest, { history, yieldRequested: false }, stream.apiObject, token);
|
||||
|
||||
// TODO: do we need to dispose the stream object?
|
||||
return {};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type * as vscode from 'vscode';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
|
||||
import { HookTypeValue } from '../../contrib/chat/common/promptSyntax/hookSchema.js';
|
||||
import { ExtHostHooksShape } from './extHost.protocol.js';
|
||||
|
||||
export const IExtHostHooks = createDecorator<IExtHostHooks>('IExtHostHooks');
|
||||
|
||||
export interface IChatHookExecutionOptions {
|
||||
readonly input?: unknown;
|
||||
readonly toolInvocationToken: unknown;
|
||||
}
|
||||
|
||||
export interface IExtHostHooks extends ExtHostHooksShape {
|
||||
executeHook(hookType: HookTypeValue, options: IChatHookExecutionOptions, token?: CancellationToken): Promise<vscode.ChatHookResult[]>;
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCom
|
||||
import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js';
|
||||
import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js';
|
||||
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
|
||||
import { HookResultKind, IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
|
||||
import { IToolInvocationContext, IToolResult, IToolResultInputOutputDetails, IToolResultOutputDetails, ToolDataSource, ToolInvocationPresentation } from '../../contrib/chat/common/tools/languageModelToolsService.js';
|
||||
import * as chatProvider from '../../contrib/chat/common/languageModels.js';
|
||||
import { IChatMessageDataPart, IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js';
|
||||
@@ -3999,3 +4000,14 @@ export namespace SourceControlInputBoxValidationType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ChatHookResult {
|
||||
export function to(result: IHookResult): vscode.ChatHookResult {
|
||||
return {
|
||||
kind: result.kind === HookResultKind.Success
|
||||
? types.ChatHookResultKind.Success
|
||||
: types.ChatHookResultKind.Error,
|
||||
result: result.result
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3935,6 +3935,15 @@ export enum SettingsSearchResultKind {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Chat Hooks
|
||||
|
||||
export enum ChatHookResultKind {
|
||||
Success = 1,
|
||||
Error = 2
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Speech
|
||||
|
||||
export enum SpeechToTextStatus {
|
||||
|
||||
@@ -31,6 +31,8 @@ import { IExtHostMpcService } from '../common/extHostMcp.js';
|
||||
import { NodeExtHostMpcService } from './extHostMcpNode.js';
|
||||
import { IExtHostAuthentication } from '../common/extHostAuthentication.js';
|
||||
import { NodeExtHostAuthentication } from './extHostAuthentication.js';
|
||||
import { IExtHostHooks } from '../common/extHostHooks.js';
|
||||
import { NodeExtHostHooks } from './extHostHooksNode.js';
|
||||
|
||||
// #########################################################################
|
||||
// ### ###
|
||||
@@ -53,3 +55,4 @@ registerSingleton(IExtHostTerminalService, ExtHostTerminalService, Instantiation
|
||||
registerSingleton(IExtHostTunnelService, NodeExtHostTunnelService, InstantiationType.Eager);
|
||||
registerSingleton(IExtHostVariableResolverProvider, NodeExtHostVariableResolverProviderService, InstantiationType.Eager);
|
||||
registerSingleton(IExtHostMpcService, NodeExtHostMpcService, InstantiationType.Eager);
|
||||
registerSingleton(IExtHostHooks, NodeExtHostHooks, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type * as vscode from 'vscode';
|
||||
import { spawn } from 'child_process';
|
||||
import { homedir } from 'os';
|
||||
import { disposableTimeout } from '../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { ILogService } from '../../../platform/log/common/log.js';
|
||||
import { HookTypeValue } from '../../contrib/chat/common/promptSyntax/hookSchema.js';
|
||||
import { isToolInvocationContext, IToolInvocationContext } from '../../contrib/chat/common/tools/languageModelToolsService.js';
|
||||
import { IHookCommandDto, MainContext, MainThreadHooksShape } from '../common/extHost.protocol.js';
|
||||
import { IChatHookExecutionOptions, IExtHostHooks } from '../common/extHostHooks.js';
|
||||
import { IExtHostRpcService } from '../common/extHostRpcService.js';
|
||||
import { HookResultKind, IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
|
||||
import * as typeConverters from '../common/extHostTypeConverters.js';
|
||||
|
||||
const SIGKILL_DELAY_MS = 5000;
|
||||
|
||||
export class NodeExtHostHooks implements IExtHostHooks {
|
||||
|
||||
private readonly _mainThreadProxy: MainThreadHooksShape;
|
||||
|
||||
constructor(
|
||||
@IExtHostRpcService extHostRpc: IExtHostRpcService,
|
||||
@ILogService private readonly _logService: ILogService
|
||||
) {
|
||||
this._mainThreadProxy = extHostRpc.getProxy(MainContext.MainThreadHooks);
|
||||
}
|
||||
|
||||
async executeHook(hookType: HookTypeValue, options: IChatHookExecutionOptions, token?: CancellationToken): Promise<vscode.ChatHookResult[]> {
|
||||
if (!options.toolInvocationToken || !isToolInvocationContext(options.toolInvocationToken)) {
|
||||
throw new Error('Invalid or missing tool invocation token');
|
||||
}
|
||||
|
||||
const context = options.toolInvocationToken as IToolInvocationContext;
|
||||
|
||||
const results = await this._mainThreadProxy.$executeHook(hookType, context.sessionResource, options.input, token ?? CancellationToken.None);
|
||||
return results.map(r => typeConverters.ChatHookResult.to({
|
||||
kind: r.kind as HookResultKind,
|
||||
result: r.result
|
||||
}));
|
||||
}
|
||||
|
||||
async $runHookCommand(hookCommand: IHookCommandDto, input: unknown, token: CancellationToken): Promise<IHookResult> {
|
||||
this._logService.debug(`[ExtHostHooks] Running hook command: ${JSON.stringify(hookCommand)}`);
|
||||
|
||||
try {
|
||||
return await this._executeCommand(hookCommand, input, token);
|
||||
} catch (err) {
|
||||
return {
|
||||
kind: HookResultKind.Error,
|
||||
result: err instanceof Error ? err.message : String(err)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _executeCommand(hook: IHookCommandDto, input: unknown, token?: CancellationToken): Promise<IHookResult> {
|
||||
const home = homedir();
|
||||
const cwdUri = hook.cwd ? URI.revive(hook.cwd) : undefined;
|
||||
const cwd = cwdUri ? cwdUri.fsPath : home;
|
||||
|
||||
// Determine command and args based on which property is specified
|
||||
// For bash/powershell: spawn the shell directly with explicit args to avoid double shell wrapping
|
||||
// For generic command: use shell=true to let the system shell handle it
|
||||
let command: string;
|
||||
let args: string[];
|
||||
let shell: boolean;
|
||||
if (hook.bash) {
|
||||
command = 'bash';
|
||||
args = ['-c', hook.bash];
|
||||
shell = false;
|
||||
} else if (hook.powershell) {
|
||||
command = 'powershell';
|
||||
args = ['-Command', hook.powershell];
|
||||
shell = false;
|
||||
} else {
|
||||
command = hook.command!;
|
||||
args = [];
|
||||
shell = true;
|
||||
}
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'pipe',
|
||||
cwd,
|
||||
env: { ...process.env, ...hook.env },
|
||||
shell,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stdout: string[] = [];
|
||||
const stderr: string[] = [];
|
||||
let exitCode: number | null = null;
|
||||
let exited = false;
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
const sigkillTimeout = disposables.add(new MutableDisposable());
|
||||
|
||||
const killWithEscalation = () => {
|
||||
if (exited) {
|
||||
return;
|
||||
}
|
||||
child.kill('SIGTERM');
|
||||
sigkillTimeout.value = disposableTimeout(() => {
|
||||
if (!exited) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}, SIGKILL_DELAY_MS);
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
exited = true;
|
||||
disposables.dispose();
|
||||
};
|
||||
|
||||
// Collect output
|
||||
child.stdout.on('data', data => stdout.push(data.toString()));
|
||||
child.stderr.on('data', data => stderr.push(data.toString()));
|
||||
|
||||
// Set up timeout (default 30 seconds)
|
||||
disposables.add(disposableTimeout(killWithEscalation, (hook.timeoutSec ?? 30) * 1000));
|
||||
|
||||
// Set up cancellation
|
||||
if (token) {
|
||||
disposables.add(token.onCancellationRequested(killWithEscalation));
|
||||
}
|
||||
|
||||
// Write input to stdin
|
||||
if (input !== undefined && input !== null) {
|
||||
try {
|
||||
child.stdin.write(JSON.stringify(input));
|
||||
} catch {
|
||||
// Ignore stdin write errors
|
||||
}
|
||||
}
|
||||
child.stdin.end();
|
||||
|
||||
// Capture exit code
|
||||
child.on('exit', code => { exitCode = code; });
|
||||
|
||||
// Resolve on close (after streams flush)
|
||||
child.on('close', () => {
|
||||
cleanup();
|
||||
const code = exitCode ?? 1;
|
||||
const stdoutStr = stdout.join('');
|
||||
const stderrStr = stderr.join('');
|
||||
|
||||
if (code === 0) {
|
||||
// Success - try to parse stdout as JSON, otherwise return as string
|
||||
let result: string | object = stdoutStr;
|
||||
try {
|
||||
result = JSON.parse(stdoutStr);
|
||||
} catch {
|
||||
// Keep as string if not valid JSON
|
||||
}
|
||||
resolve({ kind: HookResultKind.Success, result });
|
||||
} else {
|
||||
// Error
|
||||
resolve({ kind: HookResultKind.Error, result: stderrStr });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', err => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -424,6 +424,7 @@ async function lookupProxyAuthorization(
|
||||
proxyAuthenticate: string | string[] | undefined,
|
||||
state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number }
|
||||
): Promise<string | undefined> {
|
||||
proxyURL = proxyURL.replace(/\/+$/, '');
|
||||
const cached = proxyAuthenticateCache[proxyURL];
|
||||
if (proxyAuthenticate) {
|
||||
proxyAuthenticateCache[proxyURL] = proxyAuthenticate;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
import { NullLogService } from '../../../../platform/log/common/log.js';
|
||||
import { NodeExtHostHooks } from '../../node/extHostHooksNode.js';
|
||||
import { IHookCommandDto, MainThreadHooksShape } from '../../common/extHost.protocol.js';
|
||||
import { IHookResult, HookResultKind } from '../../../contrib/chat/common/hooksExecutionService.js';
|
||||
import { IExtHostRpcService } from '../../common/extHostRpcService.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
|
||||
function createHookCommandDto(command: string, options?: Partial<Omit<IHookCommandDto, 'type' | 'command'>>): IHookCommandDto {
|
||||
return {
|
||||
type: 'command',
|
||||
command,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
function createMockExtHostRpcService(mainThreadProxy: MainThreadHooksShape): IExtHostRpcService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
getProxy<T>(): T {
|
||||
return mainThreadProxy as unknown as T;
|
||||
},
|
||||
set<T, R extends T>(_identifier: unknown, instance: R): R {
|
||||
return instance;
|
||||
},
|
||||
dispose(): void { },
|
||||
assertRegistered(): void { },
|
||||
drain(): Promise<void> { return Promise.resolve(); },
|
||||
} as IExtHostRpcService;
|
||||
}
|
||||
|
||||
suite.skip('ExtHostHooks', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
let hooksService: NodeExtHostHooks;
|
||||
|
||||
setup(() => {
|
||||
const mockMainThreadProxy: MainThreadHooksShape = {
|
||||
$executeHook: async (): Promise<IHookResult[]> => {
|
||||
return [];
|
||||
},
|
||||
dispose: () => { }
|
||||
};
|
||||
|
||||
const mockRpcService = createMockExtHostRpcService(mockMainThreadProxy);
|
||||
hooksService = new NodeExtHostHooks(mockRpcService, new NullLogService());
|
||||
});
|
||||
|
||||
test('$runHookCommand runs command and returns success result', async () => {
|
||||
const hookCommand = createHookCommandDto('echo "hello world"');
|
||||
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);
|
||||
|
||||
assert.strictEqual(result.kind, HookResultKind.Success);
|
||||
assert.strictEqual((result.result as string).trim(), 'hello world');
|
||||
});
|
||||
|
||||
test('$runHookCommand parses JSON output', async () => {
|
||||
const hookCommand = createHookCommandDto('echo \'{"key": "value"}\'');
|
||||
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);
|
||||
|
||||
assert.strictEqual(result.kind, HookResultKind.Success);
|
||||
assert.deepStrictEqual(result.result, { key: 'value' });
|
||||
});
|
||||
|
||||
test('$runHookCommand returns error result for non-zero exit code', async () => {
|
||||
const hookCommand = createHookCommandDto('exit 1');
|
||||
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);
|
||||
|
||||
assert.strictEqual(result.kind, HookResultKind.Error);
|
||||
});
|
||||
|
||||
test('$runHookCommand captures stderr on failure', async () => {
|
||||
const hookCommand = createHookCommandDto('echo "error message" >&2 && exit 1');
|
||||
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);
|
||||
|
||||
assert.strictEqual(result.kind, HookResultKind.Error);
|
||||
assert.strictEqual((result.result as string).trim(), 'error message');
|
||||
});
|
||||
|
||||
test('$runHookCommand passes input to stdin as JSON', async () => {
|
||||
const hookCommand = createHookCommandDto('cat');
|
||||
const input = { tool: 'bash', args: { command: 'ls' } };
|
||||
const result = await hooksService.$runHookCommand(hookCommand, input, CancellationToken.None);
|
||||
|
||||
assert.strictEqual(result.kind, HookResultKind.Success);
|
||||
assert.deepStrictEqual(result.result, input);
|
||||
});
|
||||
|
||||
test('$runHookCommand returns error for invalid command', async () => {
|
||||
const hookCommand = createHookCommandDto('/nonexistent/command/that/does/not/exist');
|
||||
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);
|
||||
|
||||
assert.strictEqual(result.kind, HookResultKind.Error);
|
||||
});
|
||||
|
||||
test('$runHookCommand uses custom environment variables', async () => {
|
||||
const hookCommand = createHookCommandDto('echo $MY_VAR', { env: { MY_VAR: 'custom_value' } });
|
||||
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);
|
||||
|
||||
assert.strictEqual(result.kind, HookResultKind.Success);
|
||||
assert.strictEqual((result.result as string).trim(), 'custom_value');
|
||||
});
|
||||
|
||||
test('$runHookCommand uses custom cwd', async () => {
|
||||
const hookCommand = createHookCommandDto('pwd', { cwd: URI.file('/tmp') });
|
||||
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);
|
||||
|
||||
assert.strictEqual(result.kind, HookResultKind.Success);
|
||||
// The result should contain /tmp or /private/tmp (macOS symlink)
|
||||
assert.ok((result.result as string).includes('tmp'));
|
||||
});
|
||||
});
|
||||
@@ -237,11 +237,11 @@ export class CompositeBarActionViewItem extends BaseActionViewItem {
|
||||
this.container.classList.add('icon');
|
||||
}
|
||||
|
||||
// Use 'tab' inside tablist, 'button' for popup items outside tablist
|
||||
const role = this.options.isTabList || !this.options.hasPopup ? 'tab' : 'button';
|
||||
this.container.setAttribute('role', role);
|
||||
if (this.options.hasPopup) {
|
||||
this.container.setAttribute('role', 'button');
|
||||
this.container.setAttribute('aria-haspopup', 'true');
|
||||
} else {
|
||||
this.container.setAttribute('role', 'tab');
|
||||
}
|
||||
|
||||
// Try hard to prevent keyboard only focus feedback when using mouse
|
||||
@@ -479,7 +479,7 @@ export class CompositeOverflowActivityActionViewItem extends CompositeBarActionV
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
) {
|
||||
super(action, { icon: true, colors, hasPopup: true, hoverOptions }, () => true, themeService, hoverService, configurationService, keybindingService);
|
||||
super(action, { icon: true, colors, hasPopup: true, hoverOptions, isTabList: true }, () => true, themeService, hoverService, configurationService, keybindingService);
|
||||
}
|
||||
|
||||
showMenu(): void {
|
||||
|
||||
@@ -118,6 +118,7 @@ export interface IBrowserViewModel extends IDisposable {
|
||||
focus(): Promise<void>;
|
||||
findInPage(text: string, options?: IBrowserViewFindInPageOptions): Promise<void>;
|
||||
stopFindInPage(keepSelection?: boolean): Promise<void>;
|
||||
getSelectedText(): Promise<string>;
|
||||
clearStorage(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -336,6 +337,10 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
return this.browserViewService.stopFindInPage(this.id, keepSelection);
|
||||
}
|
||||
|
||||
async getSelectedText(): Promise<string> {
|
||||
return this.browserViewService.getSelectedText(this.id);
|
||||
}
|
||||
|
||||
async clearStorage(): Promise<void> {
|
||||
return this.browserViewService.clearStorage(this.id);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { RawContextKey, IContextKey, IContextKeyService } from '../../../../plat
|
||||
import { MenuId } from '../../../../platform/actions/common/actions.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { AUX_WINDOW_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
|
||||
import { IEditorOpenContext } from '../../../common/editor.js';
|
||||
import { BrowserEditorInput } from './browserEditorInput.js';
|
||||
@@ -21,7 +21,7 @@ import { IBrowserViewModel } from '../../browserView/common/browserView.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IStorageService } from '../../../../platform/storage/common/storage.js';
|
||||
import { IBrowserViewKeyDownEvent, IBrowserViewNavigationEvent, IBrowserViewLoadError } from '../../../../platform/browserView/common/browserView.js';
|
||||
import { IBrowserViewKeyDownEvent, IBrowserViewNavigationEvent, IBrowserViewLoadError, BrowserNewPageLocation } from '../../../../platform/browserView/common/browserView.js';
|
||||
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
@@ -45,6 +45,7 @@ import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { IChatRequestVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js';
|
||||
import { IBrowserTargetLocator, getDisplayNameFromOuterHTML } from '../../../../platform/browserElements/common/browserElements.js';
|
||||
import { logBrowserOpen } from './browserViewTelemetry.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
export const CONTEXT_BROWSER_CAN_GO_BACK = new RawContextKey<boolean>('browserCanGoBack', false, localize('browser.canGoBack', "Whether the browser can go back"));
|
||||
export const CONTEXT_BROWSER_CAN_GO_FORWARD = new RawContextKey<boolean>('browserCanGoForward', false, localize('browser.canGoForward', "Whether the browser can go forward"));
|
||||
@@ -342,7 +343,11 @@ export class BrowserEditor extends EditorPane {
|
||||
this.setBackgroundImage(this._model.screenshot);
|
||||
|
||||
if (context.newInGroup) {
|
||||
this.focusUrlInput();
|
||||
if (this._model.url) {
|
||||
this._browserContainer.focus();
|
||||
} else {
|
||||
this.focusUrlInput();
|
||||
}
|
||||
}
|
||||
|
||||
// Start / stop screenshots when the model visibility changes
|
||||
@@ -378,18 +383,27 @@ export class BrowserEditor extends EditorPane {
|
||||
this._devToolsOpenContext.set(e.isDevToolsOpen);
|
||||
}));
|
||||
|
||||
this._inputDisposables.add(this._model.onDidRequestNewPage(({ url, name, background }) => {
|
||||
logBrowserOpen(this.telemetryService, background ? 'browserLinkBackground' : 'browserLinkForeground');
|
||||
this._inputDisposables.add(this._model.onDidRequestNewPage(({ resource, location, position }) => {
|
||||
logBrowserOpen(this.telemetryService, (() => {
|
||||
switch (location) {
|
||||
case BrowserNewPageLocation.Background: return 'browserLinkBackground';
|
||||
case BrowserNewPageLocation.Foreground: return 'browserLinkForeground';
|
||||
case BrowserNewPageLocation.NewWindow: return 'browserLinkNewWindow';
|
||||
}
|
||||
})());
|
||||
|
||||
// Open a new browser tab for the requested URL
|
||||
const browserUri = BrowserViewUri.forUrl(url, name ? `${input.id}-${name}` : undefined);
|
||||
const targetGroup = location === BrowserNewPageLocation.NewWindow ? AUX_WINDOW_GROUP : this.group;
|
||||
this.editorService.openEditor({
|
||||
resource: browserUri,
|
||||
resource: URI.from(resource),
|
||||
options: {
|
||||
pinned: true,
|
||||
inactive: background
|
||||
inactive: location === BrowserNewPageLocation.Background,
|
||||
auxiliary: {
|
||||
bounds: position,
|
||||
compact: true
|
||||
}
|
||||
}
|
||||
}, this.group);
|
||||
}, targetGroup);
|
||||
}));
|
||||
|
||||
this._inputDisposables.add(this.overlayManager!.onDidChangeOverlayState(() => {
|
||||
@@ -586,10 +600,15 @@ export class BrowserEditor extends EditorPane {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the find widget
|
||||
* Show the find widget, optionally pre-populated with selected text from the browser view
|
||||
*/
|
||||
showFind(): void {
|
||||
this._findWidget.value.reveal();
|
||||
async showFind(): Promise<void> {
|
||||
// Get selected text from the browser view to pre-populate the search box.
|
||||
const selectedText = await this._model?.getSelectedText();
|
||||
|
||||
// Only use the selected text if it doesn't contain newlines (single line selection)
|
||||
const textToReveal = selectedText && !/[\r\n]/.test(selectedText) ? selectedText : undefined;
|
||||
this._findWidget.value.reveal(textToReveal);
|
||||
this._findWidget.value.layout(this._findWidgetContainer.clientWidth);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,14 +66,14 @@ class NewTabAction extends Action2 {
|
||||
title: localize2('browser.newTabAction', "New Tab"),
|
||||
category: BrowserCategory,
|
||||
f1: true,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: ActionGroupTabs,
|
||||
order: 1,
|
||||
},
|
||||
// When already in a browser, Ctrl/Cmd + T opens a new tab
|
||||
keybinding: {
|
||||
// When already in a browser, Ctrl/Cmd + T opens a new tab
|
||||
when: BROWSER_EDITOR_ACTIVE,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over search actions
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyT,
|
||||
}
|
||||
@@ -100,15 +100,14 @@ class GoBackAction extends Action2 {
|
||||
title: localize2('browser.goBackAction', 'Go Back'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.arrowLeft,
|
||||
f1: false,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_BACK),
|
||||
menu: {
|
||||
id: MenuId.BrowserNavigationToolbar,
|
||||
group: 'navigation',
|
||||
order: 1,
|
||||
},
|
||||
precondition: CONTEXT_BROWSER_CAN_GO_BACK,
|
||||
keybinding: {
|
||||
when: BROWSER_EDITOR_ACTIVE,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over editor navigation
|
||||
primary: KeyMod.Alt | KeyCode.LeftArrow,
|
||||
secondary: [KeyCode.BrowserBack],
|
||||
@@ -133,16 +132,15 @@ class GoForwardAction extends Action2 {
|
||||
title: localize2('browser.goForwardAction', 'Go Forward'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.arrowRight,
|
||||
f1: false,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_FORWARD),
|
||||
menu: {
|
||||
id: MenuId.BrowserNavigationToolbar,
|
||||
group: 'navigation',
|
||||
order: 2,
|
||||
when: CONTEXT_BROWSER_CAN_GO_FORWARD
|
||||
},
|
||||
precondition: CONTEXT_BROWSER_CAN_GO_FORWARD,
|
||||
keybinding: {
|
||||
when: BROWSER_EDITOR_ACTIVE,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over editor navigation
|
||||
primary: KeyMod.Alt | KeyCode.RightArrow,
|
||||
secondary: [KeyCode.BrowserForward],
|
||||
@@ -167,7 +165,8 @@ class ReloadAction extends Action2 {
|
||||
title: localize2('browser.reloadAction', 'Reload'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.refresh,
|
||||
f1: false,
|
||||
f1: true,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
menu: {
|
||||
id: MenuId.BrowserNavigationToolbar,
|
||||
group: 'navigation',
|
||||
@@ -198,9 +197,9 @@ class FocusUrlInputAction extends Action2 {
|
||||
id: FocusUrlInputAction.ID,
|
||||
title: localize2('browser.focusUrlInputAction', 'Focus URL Input'),
|
||||
category: BrowserCategory,
|
||||
f1: false,
|
||||
f1: true,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
keybinding: {
|
||||
when: BROWSER_EDITOR_ACTIVE,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
}
|
||||
@@ -222,9 +221,10 @@ class AddElementToChatAction extends Action2 {
|
||||
super({
|
||||
id: AddElementToChatAction.ID,
|
||||
title: localize2('browser.addElementToChatAction', 'Add Element to Chat'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.inspect,
|
||||
f1: false,
|
||||
precondition: enabled,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, enabled),
|
||||
toggled: CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE,
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
@@ -233,11 +233,10 @@ class AddElementToChatAction extends Action2 {
|
||||
when: enabled
|
||||
},
|
||||
keybinding: [{
|
||||
when: BROWSER_EDITOR_ACTIVE,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over terminal
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC,
|
||||
}, {
|
||||
when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE),
|
||||
when: CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyCode.Escape
|
||||
}]
|
||||
@@ -260,7 +259,8 @@ class ToggleDevToolsAction extends Action2 {
|
||||
title: localize2('browser.toggleDevToolsAction', 'Toggle Developer Tools'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.console,
|
||||
f1: false,
|
||||
f1: true,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
toggled: ContextKeyExpr.equals(CONTEXT_BROWSER_DEVTOOLS_OPEN.key, true),
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
@@ -268,7 +268,6 @@ class ToggleDevToolsAction extends Action2 {
|
||||
order: 5,
|
||||
},
|
||||
keybinding: {
|
||||
when: BROWSER_EDITOR_ACTIVE,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyCode.F12
|
||||
}
|
||||
@@ -291,7 +290,8 @@ class OpenInExternalBrowserAction extends Action2 {
|
||||
title: localize2('browser.openExternalAction', 'Open in External Browser'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.linkExternal,
|
||||
f1: false,
|
||||
f1: true,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: ActionGroupPage,
|
||||
@@ -371,7 +371,7 @@ class ClearEphemeralBrowserStorageAction extends Action2 {
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.clearAll,
|
||||
f1: true,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
precondition: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Ephemeral),
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: '3_settings',
|
||||
@@ -422,14 +422,14 @@ class ShowBrowserFindAction extends Action2 {
|
||||
id: ShowBrowserFindAction.ID,
|
||||
title: localize2('browser.showFindAction', 'Find in Page'),
|
||||
category: BrowserCategory,
|
||||
f1: false,
|
||||
f1: true,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: ActionGroupPage,
|
||||
order: 1,
|
||||
},
|
||||
keybinding: {
|
||||
when: BROWSER_EDITOR_ACTIVE,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyF
|
||||
}
|
||||
@@ -452,8 +452,8 @@ class HideBrowserFindAction extends Action2 {
|
||||
title: localize2('browser.hideFindAction', 'Close Find Widget'),
|
||||
category: BrowserCategory,
|
||||
f1: false,
|
||||
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE),
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
primary: KeyCode.Escape
|
||||
}
|
||||
@@ -477,12 +477,13 @@ class BrowserFindNextAction extends Action2 {
|
||||
title: localize2('browser.findNextAction', 'Find Next'),
|
||||
category: BrowserCategory,
|
||||
f1: false,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
keybinding: [{
|
||||
when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED),
|
||||
when: CONTEXT_BROWSER_FIND_WIDGET_FOCUSED,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyCode.Enter
|
||||
}, {
|
||||
when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE),
|
||||
when: CONTEXT_BROWSER_FIND_WIDGET_VISIBLE,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyCode.F3,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyG }
|
||||
@@ -507,12 +508,13 @@ class BrowserFindPreviousAction extends Action2 {
|
||||
title: localize2('browser.findPreviousAction', 'Find Previous'),
|
||||
category: BrowserCategory,
|
||||
f1: false,
|
||||
precondition: BROWSER_EDITOR_ACTIVE,
|
||||
keybinding: [{
|
||||
when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED),
|
||||
when: CONTEXT_BROWSER_FIND_WIDGET_FOCUSED,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyMod.Shift | KeyCode.Enter
|
||||
}, {
|
||||
when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE),
|
||||
when: CONTEXT_BROWSER_FIND_WIDGET_VISIBLE,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyMod.Shift | KeyCode.F3,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG }
|
||||
|
||||
@@ -20,10 +20,12 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet
|
||||
* opens in a new focused editor (e.g., links with target="_blank").
|
||||
* - `'browserLinkBackground'`: opened when clicking a link inside the Integrated Browser that
|
||||
* opens in a new background editor (e.g., Ctrl/Cmd+click).
|
||||
* - `'browserLinkNewWindow'`: opened when clicking a link inside the Integrated Browser that
|
||||
* opens in a new window (e.g., Shift+click).
|
||||
* - `'copyToNewWindow'`: opened when the user copies a browser editor to a new window
|
||||
* via "Copy into New Window".
|
||||
*/
|
||||
export type IntegratedBrowserOpenSource = 'commandWithoutUrl' | 'commandWithUrl' | 'newTabCommand' | 'localhostLinkOpener' | 'browserLinkForeground' | 'browserLinkBackground' | 'copyToNewWindow';
|
||||
export type IntegratedBrowserOpenSource = 'commandWithoutUrl' | 'commandWithUrl' | 'newTabCommand' | 'localhostLinkOpener' | 'browserLinkForeground' | 'browserLinkBackground' | 'browserLinkNewWindow' | 'copyToNewWindow';
|
||||
|
||||
type IntegratedBrowserOpenEvent = {
|
||||
source: IntegratedBrowserOpenSource;
|
||||
|
||||
@@ -32,7 +32,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
|
||||
import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
|
||||
import { ChatModel } from '../../common/model/chatModel.js';
|
||||
import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js';
|
||||
import { IChatService } from '../../common/chatService/chatService.js';
|
||||
import { ChatSendResult, IChatService } from '../../common/chatService/chatService.js';
|
||||
import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
|
||||
import { ChatAgentLocation } from '../../common/constants.js';
|
||||
import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js';
|
||||
@@ -289,15 +289,15 @@ export class CreateRemoteAgentJobAction {
|
||||
);
|
||||
|
||||
await chatService.removeRequest(sessionResource, addedRequest.id);
|
||||
const requestData = await chatService.sendRequest(sessionResource, userPrompt, {
|
||||
const sendResult = await chatService.sendRequest(sessionResource, userPrompt, {
|
||||
agentIdSilent: continuationTargetType,
|
||||
attachedContext: attachedContext.asArray(),
|
||||
userSelectedModelId: widget.input.currentLanguageModel,
|
||||
...widget.getModeRequestOptions()
|
||||
});
|
||||
|
||||
if (requestData) {
|
||||
await widget.handleDelegationExitIfNeeded(defaultAgent, requestData.agent);
|
||||
if (ChatSendResult.isSent(sendResult)) {
|
||||
await widget.handleDelegationExitIfNeeded(defaultAgent, sendResult.data.agent);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error creating remote coding agent job', e);
|
||||
|
||||
@@ -33,7 +33,7 @@ export function registerChatCopyActions() {
|
||||
run(accessor: ServicesAccessor, context?: ChatTreeItem) {
|
||||
const clipboardService = accessor.get(IClipboardService);
|
||||
const chatWidgetService = accessor.get(IChatWidgetService);
|
||||
const widget = (context?.sessionResource && chatWidgetService.getWidgetBySessionResource(context.sessionResource)) || chatWidgetService.lastFocusedWidget;
|
||||
const widget = ((isRequestVM(context) || isResponseVM(context)) && chatWidgetService.getWidgetBySessionResource(context.sessionResource)) || chatWidgetService.lastFocusedWidget;
|
||||
if (widget) {
|
||||
const viewModel = widget.viewModel;
|
||||
const sessionAsText = viewModel?.getItems()
|
||||
@@ -84,6 +84,10 @@ export function registerChatCopyActions() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRequestVM(item) && !isResponseVM(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = stringifyItem(item, false);
|
||||
await clipboardService.writeText(text);
|
||||
}
|
||||
|
||||
@@ -822,7 +822,8 @@ export class CancelAction extends Action2 {
|
||||
id: MenuId.ChatExecute,
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.requestInProgress,
|
||||
ChatContextKeys.remoteJobCreating.negate()
|
||||
ChatContextKeys.remoteJobCreating.negate(),
|
||||
ChatContextKeys.currentlyEditing.negate(),
|
||||
),
|
||||
order: 4,
|
||||
group: 'navigation',
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
|
||||
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
|
||||
import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js';
|
||||
import { ChatConfiguration } from '../../common/constants.js';
|
||||
import { isRequestVM } from '../../common/model/chatViewModel.js';
|
||||
import { IChatWidgetService } from '../chat.js';
|
||||
import { CHAT_CATEGORY } from './chatActions.js';
|
||||
|
||||
const queueingEnabledCondition = ContextKeyExpr.equals(`config.${ChatConfiguration.RequestQueueingEnabled}`, true);
|
||||
|
||||
export interface IChatRemovePendingRequestContext {
|
||||
sessionResource: URI;
|
||||
pendingRequestId: string;
|
||||
}
|
||||
|
||||
function isRemovePendingRequestContext(context: unknown): context is IChatRemovePendingRequestContext {
|
||||
return !!context &&
|
||||
typeof context === 'object' &&
|
||||
'sessionResource' in context &&
|
||||
'pendingRequestId' in context &&
|
||||
URI.isUri((context as IChatRemovePendingRequestContext).sessionResource) &&
|
||||
typeof (context as IChatRemovePendingRequestContext).pendingRequestId === 'string';
|
||||
}
|
||||
|
||||
export class ChatQueueMessageAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.chat.queueMessage';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ChatQueueMessageAction.ID,
|
||||
title: localize2('chat.queueMessage', "Add to Queue"),
|
||||
tooltip: localize('chat.queueMessage.tooltip', "Queue this message to send after the current request completes"),
|
||||
icon: Codicon.add,
|
||||
f1: false,
|
||||
category: CHAT_CATEGORY,
|
||||
precondition: ContextKeyExpr.and(
|
||||
queueingEnabledCondition,
|
||||
ChatContextKeys.requestInProgress,
|
||||
ChatContextKeys.inputHasText
|
||||
),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.inChatInput,
|
||||
ChatContextKeys.requestInProgress,
|
||||
queueingEnabledCondition
|
||||
),
|
||||
primary: KeyCode.Enter,
|
||||
weight: KeybindingWeight.EditorContrib + 1
|
||||
},
|
||||
menu: [{
|
||||
id: MenuId.ChatExecuteQueue,
|
||||
group: 'navigation',
|
||||
order: 1,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
|
||||
const widgetService = accessor.get(IChatWidgetService);
|
||||
const widget = widgetService.lastFocusedWidget;
|
||||
if (!widget?.viewModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputValue = widget.getInput();
|
||||
if (!inputValue.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.acceptInput(undefined, { queue: ChatRequestQueueKind.Queued });
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatSteerWithMessageAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.chat.steerWithMessage';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ChatSteerWithMessageAction.ID,
|
||||
title: localize2('chat.steerWithMessage', "Steer with Message"),
|
||||
tooltip: localize('chat.steerWithMessage.tooltip', "Send this message at the next opportunity, signaling the current request to yield"),
|
||||
icon: Codicon.arrowRight,
|
||||
f1: false,
|
||||
category: CHAT_CATEGORY,
|
||||
precondition: ContextKeyExpr.and(
|
||||
queueingEnabledCondition,
|
||||
ChatContextKeys.requestInProgress,
|
||||
ChatContextKeys.inputHasText
|
||||
),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.inChatInput,
|
||||
ChatContextKeys.requestInProgress,
|
||||
queueingEnabledCondition
|
||||
),
|
||||
primary: KeyMod.Alt | KeyCode.Enter,
|
||||
weight: KeybindingWeight.EditorContrib + 1
|
||||
},
|
||||
menu: [{
|
||||
id: MenuId.ChatExecuteQueue,
|
||||
group: 'navigation',
|
||||
order: 2,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
|
||||
const widgetService = accessor.get(IChatWidgetService);
|
||||
const widget = widgetService.lastFocusedWidget;
|
||||
if (!widget?.viewModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputValue = widget.getInput();
|
||||
if (!inputValue.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.acceptInput(undefined, { queue: ChatRequestQueueKind.Steering });
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatRemovePendingRequestAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.chat.removePendingRequest';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ChatRemovePendingRequestAction.ID,
|
||||
title: localize2('chat.removePendingRequest', "Remove from Queue"),
|
||||
icon: Codicon.close,
|
||||
f1: false,
|
||||
category: CHAT_CATEGORY,
|
||||
menu: [{
|
||||
id: MenuId.ChatMessageTitle,
|
||||
group: 'navigation',
|
||||
order: 4,
|
||||
when: ContextKeyExpr.and(
|
||||
queueingEnabledCondition,
|
||||
ChatContextKeys.isRequest,
|
||||
ChatContextKeys.isPendingRequest
|
||||
)
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
|
||||
const chatService = accessor.get(IChatService);
|
||||
const [context] = args;
|
||||
|
||||
// Support both toolbar context (IChatRequestViewModel) and command context (IChatRemovePendingRequestContext)
|
||||
if (isRequestVM(context) && context.pendingKind) {
|
||||
chatService.removePendingRequest(context.sessionResource, context.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRemovePendingRequestContext(context)) {
|
||||
chatService.removePendingRequest(context.sessionResource, context.pendingRequestId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatSendPendingImmediatelyAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.chat.sendPendingImmediately';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ChatSendPendingImmediatelyAction.ID,
|
||||
title: localize2('chat.sendPendingImmediately', "Send Immediately"),
|
||||
icon: Codicon.arrowUp,
|
||||
f1: false,
|
||||
category: CHAT_CATEGORY,
|
||||
menu: [{
|
||||
id: MenuId.ChatMessageTitle,
|
||||
group: 'navigation',
|
||||
order: 3,
|
||||
when: ContextKeyExpr.and(
|
||||
queueingEnabledCondition,
|
||||
ChatContextKeys.isRequest,
|
||||
ChatContextKeys.isPendingRequest
|
||||
)
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
|
||||
const chatService = accessor.get(IChatService);
|
||||
const widgetService = accessor.get(IChatWidgetService);
|
||||
const [context] = args;
|
||||
|
||||
if (!isRequestVM(context) || !context.pendingKind) {
|
||||
return;
|
||||
}
|
||||
|
||||
const widget = widgetService.getWidgetBySessionResource(context.sessionResource);
|
||||
const model = widget?.viewModel?.model;
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingRequests = model.getPendingRequests();
|
||||
const targetIndex = pendingRequests.findIndex(r => r.request.id === context.id);
|
||||
if (targetIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep the target item's kind (queued vs steering)
|
||||
const targetRequest = pendingRequests[targetIndex];
|
||||
|
||||
// Reorder: move target to front, keep others in their relative order
|
||||
const reordered = [
|
||||
{ requestId: targetRequest.request.id, kind: targetRequest.kind },
|
||||
...pendingRequests.filter((_, i) => i !== targetIndex).map(r => ({ requestId: r.request.id, kind: r.kind }))
|
||||
];
|
||||
|
||||
chatService.setPendingRequests(context.sessionResource, reordered);
|
||||
chatService.cancelCurrentRequestForSession(context.sessionResource);
|
||||
chatService.processPendingRequests(context.sessionResource);
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatRemoveAllPendingRequestsAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.chat.removeAllPendingRequests';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ChatRemoveAllPendingRequestsAction.ID,
|
||||
title: localize2('chat.removeAllPendingRequests', "Remove All Queued"),
|
||||
icon: Codicon.clearAll,
|
||||
f1: false,
|
||||
category: CHAT_CATEGORY,
|
||||
menu: [{
|
||||
id: MenuId.ChatContext,
|
||||
group: 'navigation',
|
||||
order: 3,
|
||||
when: ContextKeyExpr.and(
|
||||
queueingEnabledCondition,
|
||||
ChatContextKeys.hasPendingRequests
|
||||
)
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
|
||||
const chatService = accessor.get(IChatService);
|
||||
const widgetService = accessor.get(IChatWidgetService);
|
||||
const [context] = args;
|
||||
|
||||
const widget = (isRequestVM(context) && widgetService.getWidgetBySessionResource(context.sessionResource)) || widgetService.lastFocusedWidget;
|
||||
const model = widget?.viewModel?.model;
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const pendingRequest of [...model.getPendingRequests()]) {
|
||||
chatService.removePendingRequest(model.sessionResource, pendingRequest.request.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerChatQueueActions(): void {
|
||||
registerAction2(ChatQueueMessageAction);
|
||||
registerAction2(ChatSteerWithMessageAction);
|
||||
registerAction2(ChatRemovePendingRequestAction);
|
||||
registerAction2(ChatSendPendingImmediatelyAction);
|
||||
registerAction2(ChatRemoveAllPendingRequestsAction);
|
||||
|
||||
// Register the queue submenu as a split button dropdown in the execute toolbar
|
||||
// This shows "Add to Queue" / "Steer with Message" when a request is in progress and input has text
|
||||
MenuRegistry.appendMenuItem(MenuId.ChatExecute, {
|
||||
submenu: MenuId.ChatExecuteQueue,
|
||||
title: localize2('chat.queueSubmenu', "Queue"),
|
||||
icon: Codicon.listOrdered,
|
||||
when: ContextKeyExpr.and(
|
||||
queueingEnabledCondition,
|
||||
ChatContextKeys.requestInProgress,
|
||||
ChatContextKeys.inputHasText
|
||||
),
|
||||
group: 'navigation',
|
||||
order: 4,
|
||||
isSplitButton: { togglePrimaryAction: true }
|
||||
});
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export enum AgentSessionsGrouping {
|
||||
|
||||
export interface IAgentSessionsFilterOptions extends Partial<IAgentSessionsFilter> {
|
||||
|
||||
readonly filterMenuId: MenuId;
|
||||
readonly filterMenuId?: MenuId;
|
||||
|
||||
readonly limitResults?: () => number | undefined;
|
||||
notifyResults?(count: number): void;
|
||||
@@ -41,7 +41,7 @@ const DEFAULT_EXCLUDES: IAgentSessionsFilterExcludes = Object.freeze({
|
||||
|
||||
export class AgentSessionsFilter extends Disposable implements Required<IAgentSessionsFilter> {
|
||||
|
||||
private readonly STORAGE_KEY: string;
|
||||
private readonly STORAGE_KEY = `agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu`;
|
||||
|
||||
private readonly _onDidChange = this._register(new Emitter<void>());
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
@@ -61,8 +61,6 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
|
||||
) {
|
||||
super();
|
||||
|
||||
this.STORAGE_KEY = `agentSessions.filterExcludes.${this.options.filterMenuId.id.toLowerCase()}`;
|
||||
|
||||
this.updateExcludes(false);
|
||||
|
||||
this.registerListeners();
|
||||
@@ -116,14 +114,19 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
|
||||
private updateFilterActions(): void {
|
||||
this.actionDisposables.clear();
|
||||
|
||||
this.registerProviderActions(this.actionDisposables);
|
||||
this.registerStateActions(this.actionDisposables);
|
||||
this.registerArchivedActions(this.actionDisposables);
|
||||
this.registerReadActions(this.actionDisposables);
|
||||
this.registerResetAction(this.actionDisposables);
|
||||
const menuId = this.options.filterMenuId;
|
||||
if (!menuId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerProviderActions(this.actionDisposables, menuId);
|
||||
this.registerStateActions(this.actionDisposables, menuId);
|
||||
this.registerArchivedActions(this.actionDisposables, menuId);
|
||||
this.registerReadActions(this.actionDisposables, menuId);
|
||||
this.registerResetAction(this.actionDisposables, menuId);
|
||||
}
|
||||
|
||||
private registerProviderActions(disposables: DisposableStore): void {
|
||||
private registerProviderActions(disposables: DisposableStore, menuId: MenuId): void {
|
||||
const providers: { id: string; label: string }[] = Object.values(AgentSessionProviders).map(provider => ({
|
||||
id: provider,
|
||||
label: getAgentSessionProviderName(provider)
|
||||
@@ -143,10 +146,10 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
|
||||
disposables.add(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: `agentSessions.filter.toggleExclude:${provider.id}.${that.options.filterMenuId.id.toLowerCase()}`,
|
||||
id: `agentSessions.filter.toggleExclude:${provider.id}.${menuId.id.toLowerCase()}`,
|
||||
title: provider.label,
|
||||
menu: {
|
||||
id: that.options.filterMenuId,
|
||||
id: menuId,
|
||||
group: '1_providers',
|
||||
order: counter++,
|
||||
},
|
||||
@@ -165,7 +168,7 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
|
||||
}
|
||||
}
|
||||
|
||||
private registerStateActions(disposables: DisposableStore): void {
|
||||
private registerStateActions(disposables: DisposableStore, menuId: MenuId): void {
|
||||
const states: { id: AgentSessionStatus; label: string }[] = [
|
||||
{ id: AgentSessionStatus.Completed, label: localize('agentSessionStatus.completed', "Completed") },
|
||||
{ id: AgentSessionStatus.InProgress, label: localize('agentSessionStatus.inProgress', "In Progress") },
|
||||
@@ -179,10 +182,10 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
|
||||
disposables.add(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: `agentSessions.filter.toggleExcludeState:${state.id}.${that.options.filterMenuId.id.toLowerCase()}`,
|
||||
id: `agentSessions.filter.toggleExcludeState:${state.id}.${menuId.id.toLowerCase()}`,
|
||||
title: state.label,
|
||||
menu: {
|
||||
id: that.options.filterMenuId,
|
||||
id: menuId,
|
||||
group: '2_states',
|
||||
order: counter++,
|
||||
},
|
||||
@@ -201,15 +204,15 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
|
||||
}
|
||||
}
|
||||
|
||||
private registerArchivedActions(disposables: DisposableStore): void {
|
||||
private registerArchivedActions(disposables: DisposableStore, menuId: MenuId): void {
|
||||
const that = this;
|
||||
disposables.add(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: `agentSessions.filter.toggleExcludeArchived.${that.options.filterMenuId.id.toLowerCase()}`,
|
||||
id: `agentSessions.filter.toggleExcludeArchived.${menuId.id.toLowerCase()}`,
|
||||
title: localize('agentSessions.filter.archived', 'Archived'),
|
||||
menu: {
|
||||
id: that.options.filterMenuId,
|
||||
id: menuId,
|
||||
group: '3_props',
|
||||
order: 1000,
|
||||
},
|
||||
@@ -222,15 +225,15 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
|
||||
}));
|
||||
}
|
||||
|
||||
private registerReadActions(disposables: DisposableStore): void {
|
||||
private registerReadActions(disposables: DisposableStore, menuId: MenuId): void {
|
||||
const that = this;
|
||||
disposables.add(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: `agentSessions.filter.toggleExcludeRead.${that.options.filterMenuId.id.toLowerCase()}`,
|
||||
id: `agentSessions.filter.toggleExcludeRead.${menuId.id.toLowerCase()}`,
|
||||
title: localize('agentSessions.filter.read', 'Read'),
|
||||
menu: {
|
||||
id: that.options.filterMenuId,
|
||||
id: menuId,
|
||||
group: '3_props',
|
||||
order: 0,
|
||||
},
|
||||
@@ -243,15 +246,15 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
|
||||
}));
|
||||
}
|
||||
|
||||
private registerResetAction(disposables: DisposableStore): void {
|
||||
private registerResetAction(disposables: DisposableStore, menuId: MenuId): void {
|
||||
const that = this;
|
||||
disposables.add(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: `agentSessions.filter.resetExcludes.${that.options.filterMenuId.id.toLowerCase()}`,
|
||||
id: `agentSessions.filter.resetExcludes.${menuId.id.toLowerCase()}`,
|
||||
title: localize('agentSessions.filter.reset', "Reset"),
|
||||
menu: {
|
||||
id: that.options.filterMenuId,
|
||||
id: menuId,
|
||||
group: '4_reset',
|
||||
order: 0,
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { safeStringify } from '../../../../../base/common/objects.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { URI, UriComponents } from '../../../../../base/common/uri.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
@@ -425,9 +426,9 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode
|
||||
private registerListeners(): void {
|
||||
|
||||
// Sessions changes
|
||||
this._register(this.chatSessionsService.onDidChangeItemsProviders(({ chatSessionType: provider }) => this.resolve(provider)));
|
||||
this._register(this.chatSessionsService.onDidChangeItemsProviders(({ chatSessionType }) => this.resolve(chatSessionType)));
|
||||
this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined)));
|
||||
this._register(this.chatSessionsService.onDidChangeSessionItems(provider => this.resolve(provider)));
|
||||
this._register(this.chatSessionsService.onDidChangeSessionItems(({ chatSessionType }) => this.resolve(chatSessionType)));
|
||||
|
||||
// State
|
||||
this._register(this.storageService.onWillSaveState(() => {
|
||||
@@ -725,7 +726,7 @@ class AgentSessionsCache {
|
||||
metadata: session.metadata
|
||||
} satisfies ISerializedAgentSession));
|
||||
|
||||
this.storageService.store(AgentSessionsCache.SESSIONS_STORAGE_KEY, JSON.stringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE);
|
||||
this.storageService.store(AgentSessionsCache.SESSIONS_STORAGE_KEY, safeStringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE);
|
||||
}
|
||||
|
||||
loadCachedSessions(): IInternalAgentSessionData[] {
|
||||
@@ -764,6 +765,7 @@ class AgentSessionsCache {
|
||||
insertions: change.insertions,
|
||||
deletions: change.deletions,
|
||||
})) : session.changes,
|
||||
metadata: session.metadata,
|
||||
}));
|
||||
} catch {
|
||||
return []; // invalid data in storage, fallback to empty sessions list
|
||||
|
||||
@@ -16,6 +16,7 @@ import { IAgentSession, isLocalAgentSessionItem } from './agentSessionsModel.js'
|
||||
import { IAgentSessionsService } from './agentSessionsService.js';
|
||||
import { AgentSessionsSorter, groupAgentSessionsByDate, sessionDateFromNow } from './agentSessionsViewer.js';
|
||||
import { AGENT_SESSION_DELETE_ACTION_ID, AGENT_SESSION_RENAME_ACTION_ID, getAgentSessionTime } from './agentSessions.js';
|
||||
import { AgentSessionsFilter } from './agentSessionsFilter.js';
|
||||
|
||||
interface ISessionPickItem extends IQuickPickItem {
|
||||
readonly session: IAgentSession;
|
||||
@@ -75,8 +76,9 @@ export class AgentSessionsPicker {
|
||||
async pickAgentSession(): Promise<void> {
|
||||
const disposables = new DisposableStore();
|
||||
const picker = disposables.add(this.quickInputService.createQuickPick<ISessionPickItem>({ useSeparators: true }));
|
||||
const filter = disposables.add(this.instantiationService.createInstance(AgentSessionsFilter, {}));
|
||||
|
||||
picker.items = this.createPickerItems();
|
||||
picker.items = this.createPickerItems(filter);
|
||||
picker.canAcceptInBackground = true;
|
||||
picker.placeholder = localize('chatAgentPickerPlaceholder', "Search agent sessions by name");
|
||||
|
||||
@@ -116,7 +118,7 @@ export class AgentSessionsPicker {
|
||||
await this.agentSessionsService.model.resolve(session.providerType);
|
||||
this.pickAgentSession();
|
||||
} else {
|
||||
picker.items = this.createPickerItems();
|
||||
picker.items = this.createPickerItems(filter);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -124,8 +126,10 @@ export class AgentSessionsPicker {
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private createPickerItems(): (ISessionPickItem | IQuickPickSeparator)[] {
|
||||
const sessions = this.agentSessionsService.model.sessions.sort(this.sorter.compare.bind(this.sorter));
|
||||
private createPickerItems(filter: AgentSessionsFilter): (ISessionPickItem | IQuickPickSeparator)[] {
|
||||
const sessions = this.agentSessionsService.model.sessions
|
||||
.filter(session => !filter.exclude(session))
|
||||
.sort(this.sorter.compare.bind(this.sorter));
|
||||
const items: (ISessionPickItem | IQuickPickSeparator)[] = [];
|
||||
|
||||
const groupedSessions = groupAgentSessionsByDate(sessions);
|
||||
|
||||
@@ -16,12 +16,14 @@ import { openSession } from './agentSessionsOpener.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { AGENT_SESSION_DELETE_ACTION_ID, AGENT_SESSION_RENAME_ACTION_ID } from './agentSessions.js';
|
||||
import { archiveButton, deleteButton, getSessionButtons, getSessionDescription, renameButton, unarchiveButton } from './agentSessionsPicker.js';
|
||||
import { AgentSessionsFilter } from './agentSessionsFilter.js';
|
||||
|
||||
export const AGENT_SESSIONS_QUICK_ACCESS_PREFIX = 'agent ';
|
||||
|
||||
export class AgentSessionsQuickAccessProvider extends PickerQuickAccessProvider<IPickerQuickAccessItem> {
|
||||
|
||||
private readonly sorter = new AgentSessionsSorter();
|
||||
private readonly filter: AgentSessionsFilter;
|
||||
|
||||
constructor(
|
||||
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
|
||||
@@ -34,12 +36,16 @@ export class AgentSessionsQuickAccessProvider extends PickerQuickAccessProvider<
|
||||
label: localize('noAgentSessionResults', "No matching agent sessions")
|
||||
}
|
||||
});
|
||||
|
||||
this.filter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, {}));
|
||||
}
|
||||
|
||||
protected async _getPicks(filter: string): Promise<(IQuickPickSeparator | IPickerQuickAccessItem)[]> {
|
||||
const picks: Array<IPickerQuickAccessItem | IQuickPickSeparator> = [];
|
||||
|
||||
const sessions = this.agentSessionsService.model.sessions.sort(this.sorter.compare.bind(this.sorter));
|
||||
const sessions = this.agentSessionsService.model.sessions
|
||||
.filter(session => !this.filter.exclude(session))
|
||||
.sort(this.sorter.compare.bind(this.sorter));
|
||||
const groupedSessions = groupAgentSessionsByDate(sessions);
|
||||
|
||||
for (const group of groupedSessions.values()) {
|
||||
|
||||
+1
-1
@@ -749,7 +749,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem {
|
||||
// Create dropdown action (empty label prevents default tooltip - we have our own hover)
|
||||
const dropdownAction = toAction({
|
||||
id: 'agentStatus.sparkle.dropdown',
|
||||
label: '',
|
||||
label: localize('agentStatus.sparkle.dropdown', "More Actions"),
|
||||
run() { }
|
||||
});
|
||||
|
||||
|
||||
+97
-16
@@ -89,6 +89,7 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
private _tabBarContainer: HTMLElement | undefined;
|
||||
private _isInternalValueChange = false; // Flag to prevent recursive tab detection
|
||||
private _isUpdatingSendToAgent = false; // Guard to prevent infinite loop
|
||||
private _arrivedViaShortcut: '<' | '>' | undefined; // Track if we arrived at current tab via shortcut key
|
||||
private _sendToAgentTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
private _sendButton: HTMLButtonElement | undefined;
|
||||
private _sendButtonLabel: HTMLSpanElement | undefined;
|
||||
@@ -158,6 +159,20 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
if (this._isInternalValueChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user removed the shortcut character (including when input is emptied) - switch back to Files
|
||||
if (this._arrivedViaShortcut) {
|
||||
const shortcut = this._arrivedViaShortcut;
|
||||
if (!value.startsWith(shortcut)) {
|
||||
const filesTab = this._tabs.find(t => t.id === 'files');
|
||||
if (filesTab && filesTab !== this._currentTab) {
|
||||
this._arrivedViaShortcut = undefined;
|
||||
this._switchTab(filesTab, picker, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const matchingTab = this._detectTabFromValue(value);
|
||||
if (matchingTab && matchingTab !== this._currentTab) {
|
||||
this._switchTab(matchingTab, picker, true);
|
||||
@@ -185,10 +200,15 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
(item as IQuickPickItem & { id?: string }).id !== SEND_TO_AGENT_ID
|
||||
);
|
||||
|
||||
// Get the filter text
|
||||
const filterText = this._currentTab
|
||||
? picker.value.substring(this._currentTab.prefix.length).trim()
|
||||
: picker.value.trim();
|
||||
// Get the filter text (without prefix or shortcut character)
|
||||
let filterText: string;
|
||||
if (this._arrivedViaShortcut && picker.value.startsWith(this._arrivedViaShortcut)) {
|
||||
filterText = picker.value.substring(1).trim();
|
||||
} else if (this._currentTab) {
|
||||
filterText = picker.value.substring(this._currentTab.prefix.length).trim();
|
||||
} else {
|
||||
filterText = picker.value.trim();
|
||||
}
|
||||
|
||||
// Send to agent if:
|
||||
// 1. Send-to-agent item is explicitly selected, OR
|
||||
@@ -205,6 +225,7 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
this._providerCts = undefined;
|
||||
this._currentPicker = undefined;
|
||||
this._currentTab = undefined;
|
||||
this._arrivedViaShortcut = undefined;
|
||||
// Clear any pending timeout
|
||||
if (this._sendToAgentTimeout) {
|
||||
clearTimeout(this._sendToAgentTimeout);
|
||||
@@ -407,12 +428,17 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the current message to a new agent session (strips prefix).
|
||||
* Send the current message to a new agent session (strips prefix or shortcut character).
|
||||
*/
|
||||
private async _sendMessage(value: string): Promise<void> {
|
||||
// Strip any prefix from the value
|
||||
// Strip any prefix or shortcut character from the value
|
||||
let message = value;
|
||||
if (this._currentTab) {
|
||||
|
||||
// First, strip shortcut character if we arrived via shortcut
|
||||
if (this._arrivedViaShortcut && message.startsWith(this._arrivedViaShortcut)) {
|
||||
message = message.substring(1).trim();
|
||||
} else if (this._currentTab) {
|
||||
// Otherwise strip the normal prefix
|
||||
if (value.startsWith(this._currentTab.prefix)) {
|
||||
message = value.substring(this._currentTab.prefix.length).trim();
|
||||
}
|
||||
@@ -446,10 +472,16 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the filter text (without prefix)
|
||||
const filterText = this._currentTab
|
||||
? picker.value.substring(this._currentTab.prefix.length).trim()
|
||||
: picker.value.trim();
|
||||
// Get the filter text (without prefix or shortcut character)
|
||||
let filterText: string;
|
||||
if (this._arrivedViaShortcut && picker.value.startsWith(this._arrivedViaShortcut)) {
|
||||
// Strip shortcut character
|
||||
filterText = picker.value.substring(1).trim();
|
||||
} else if (this._currentTab) {
|
||||
filterText = picker.value.substring(this._currentTab.prefix.length).trim();
|
||||
} else {
|
||||
filterText = picker.value.trim();
|
||||
}
|
||||
|
||||
// Use full input if filter text is empty but there's input (user typed without prefix)
|
||||
const fullInput = picker.value.trim();
|
||||
@@ -529,16 +561,40 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
// Update picker value (with flag to prevent recursive tab detection)
|
||||
this._isInternalValueChange = true;
|
||||
if (preserveFilterText && previousTab) {
|
||||
// User typed a prefix - keep the filter text, just change prefix
|
||||
const filterText = picker.value.substring(previousTab.prefix.length);
|
||||
picker.value = tab.prefix + filterText;
|
||||
// User typed a shortcut prefix - normalize the value to show just the shortcut character
|
||||
const currentValue = picker.value;
|
||||
|
||||
// Strip previous tab's prefix if present
|
||||
let filterText = currentValue;
|
||||
if (currentValue.startsWith(previousTab.prefix)) {
|
||||
filterText = currentValue.substring(previousTab.prefix.length);
|
||||
}
|
||||
|
||||
// Handle shortcut transitions - ensure only one shortcut char is shown
|
||||
if (this._arrivedViaShortcut === '<' && tab.id === 'agentSessions') {
|
||||
// Strip any leading "<" chars and set just one
|
||||
filterText = filterText.replace(/^<+/, '');
|
||||
picker.value = '<' + filterText;
|
||||
} else if (this._arrivedViaShortcut === '>' && tab.id === 'commands') {
|
||||
// Strip any leading ">" chars and set just one
|
||||
filterText = filterText.replace(/^>+/, '');
|
||||
picker.value = '>' + filterText;
|
||||
} else {
|
||||
// Normal prefix-based switching
|
||||
picker.value = tab.prefix + filterText;
|
||||
}
|
||||
} else if (previousTab) {
|
||||
// User clicked tab - keep current text but strip old prefix (don't add new prefix)
|
||||
const currentValue = picker.value;
|
||||
if (currentValue.startsWith(previousTab.prefix)) {
|
||||
picker.value = currentValue.substring(previousTab.prefix.length);
|
||||
}
|
||||
// else: keep current value as-is
|
||||
// Also strip shortcut character if present
|
||||
if (picker.value.startsWith('<') || picker.value.startsWith('>')) {
|
||||
picker.value = picker.value.substring(1);
|
||||
}
|
||||
// Clear shortcut tracking when switching via click
|
||||
this._arrivedViaShortcut = undefined;
|
||||
}
|
||||
// else: first tab activation, value already set
|
||||
this._isInternalValueChange = false;
|
||||
@@ -552,8 +608,27 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
/**
|
||||
* Detect which tab matches the current value based on prefix.
|
||||
* Only switches away from current tab if user explicitly typed a different prefix.
|
||||
* Supports shortcut keys: ">" for Commands, "<" for Sessions.
|
||||
*/
|
||||
private _detectTabFromValue(value: string): IUnifiedQuickAccessTab | undefined {
|
||||
// Check for "<" shortcut to switch to Sessions (from Files or Commands)
|
||||
if (value === '<' || value.startsWith('<')) {
|
||||
const sessionsTab = this._tabs.find(t => t.id === 'agentSessions');
|
||||
if (sessionsTab && this._currentTab?.id !== 'agentSessions') {
|
||||
this._arrivedViaShortcut = '<';
|
||||
return sessionsTab;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ">" shortcut to switch to Commands (from Files or Sessions)
|
||||
if (value === '>' || value.startsWith('>')) {
|
||||
const commandsTab = this._tabs.find(t => t.id === 'commands');
|
||||
if (commandsTab && this._currentTab?.id !== 'commands') {
|
||||
this._arrivedViaShortcut = '>';
|
||||
return commandsTab;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't auto-switch if current tab matches (user is just typing)
|
||||
if (this._currentTab && value.startsWith(this._currentTab.prefix)) {
|
||||
return this._currentTab;
|
||||
@@ -596,9 +671,15 @@ export class UnifiedQuickAccess extends Disposable {
|
||||
const [provider] = this._getOrInstantiateProvider(tab.prefix);
|
||||
|
||||
if (provider) {
|
||||
// Configure filtering - strip the tab's prefix from the filter value
|
||||
// Configure filtering - strip the tab's prefix or shortcut character from the filter value
|
||||
const tabPrefix = tab.prefix;
|
||||
const arrivedViaShortcut = this._arrivedViaShortcut;
|
||||
picker.filterValue = (value: string) => {
|
||||
// If arrived via shortcut, strip the shortcut character
|
||||
if (arrivedViaShortcut && value.startsWith(arrivedViaShortcut)) {
|
||||
return value.substring(1);
|
||||
}
|
||||
// Otherwise strip the normal prefix
|
||||
if (value.startsWith(tabPrefix)) {
|
||||
return value.substring(tabPrefix.length);
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess
|
||||
() => this._onDidChangeChatSessionItems.fire()
|
||||
));
|
||||
|
||||
this._register(this.chatSessionsService.onDidChangeSessionItems(sessionType => {
|
||||
if (sessionType === this.chatSessionType) {
|
||||
this._register(this.chatSessionsService.onDidChangeSessionItems(({ chatSessionType }) => {
|
||||
if (chatSessionType === this.chatSessionType) {
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -37,6 +37,8 @@ import { IAction, toAction } from '../../../../../base/common/actions.js';
|
||||
import { WebviewInput } from '../../../webviewPanel/browser/webviewEditorInput.js';
|
||||
import { IBrowserTargetLocator, getDisplayNameFromOuterHTML } from '../../../../../platform/browserElements/common/browserElements.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
|
||||
import { observableConfigValue, observableContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js';
|
||||
|
||||
type BrowserType = 'simpleBrowser' | 'livePreview';
|
||||
|
||||
@@ -366,12 +368,9 @@ class SimpleBrowserOverlayController {
|
||||
@IInstantiationService instaService: IInstantiationService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IBrowserElementsService private readonly _browserElementsService: IBrowserElementsService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
) {
|
||||
|
||||
if (!this.configurationService.getValue('chat.sendElementsToChat.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._domNode.classList.add('chat-simple-browser-overlay');
|
||||
this._domNode.style.position = 'absolute';
|
||||
this._domNode.style.bottom = `5px`;
|
||||
@@ -444,11 +443,18 @@ class SimpleBrowserOverlayController {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// Observe chat enabled state and sendElementsToChat configuration
|
||||
const chatEnabledObs = observableContextKey<boolean>(ChatContextKeys.enabled.key, this.contextKeyService);
|
||||
const sendElementsEnabledObs = observableConfigValue<boolean>('chat.sendElementsToChat.enabled', true, this.configurationService);
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
|
||||
const activeEditor = activeIdObs.read(r);
|
||||
const isChatEnabled = chatEnabledObs.read(r);
|
||||
const isSendElementsEnabled = sendElementsEnabledObs.read(r);
|
||||
|
||||
if (!activeEditor) {
|
||||
// Hide if chat is not enabled, sendElementsToChat is not enabled, or no active editor
|
||||
if (!isChatEnabled || !isSendElementsEnabled || !activeEditor) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,11 +52,14 @@ import { ILanguageModelsService, LanguageModelsService } from '../common/languag
|
||||
import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js';
|
||||
import { ILanguageModelToolsConfirmationService } from '../common/tools/languageModelToolsConfirmationService.js';
|
||||
import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js';
|
||||
import { HooksExecutionService, IHooksExecutionService } from '../common/hooksExecutionService.js';
|
||||
import { ChatPromptFilesExtensionPointHandler } from '../common/promptSyntax/chatPromptFilesContribution.js';
|
||||
import { PromptsConfig } from '../common/promptSyntax/config/config.js';
|
||||
import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME } from '../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME, DEFAULT_HOOK_FILE_PATHS } from '../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { PromptLanguageFeaturesProvider } from '../common/promptSyntax/promptFileContributions.js';
|
||||
import { AGENT_DOCUMENTATION_URL, INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL } from '../common/promptSyntax/promptTypes.js';
|
||||
import { AGENT_DOCUMENTATION_URL, INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL, HOOK_DOCUMENTATION_URL } from '../common/promptSyntax/promptTypes.js';
|
||||
import { hookFileSchema, HOOK_SCHEMA_URI, HOOK_FILE_GLOB } from '../common/promptSyntax/hookSchema.js';
|
||||
import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';
|
||||
import { IPromptsService } from '../common/promptSyntax/service/promptsService.js';
|
||||
import { PromptsService } from '../common/promptSyntax/service/promptsServiceImpl.js';
|
||||
import { LanguageModelToolsExtensionPointHandler } from '../common/tools/languageModelToolsContribution.js';
|
||||
@@ -78,6 +81,7 @@ import { registerLanguageModelActions } from './actions/chatLanguageModelActions
|
||||
import { registerMoveActions } from './actions/chatMoveActions.js';
|
||||
import { registerNewChatActions } from './actions/chatNewActions.js';
|
||||
import { registerChatPromptNavigationActions } from './actions/chatPromptNavigationActions.js';
|
||||
import { registerChatQueueActions } from './actions/chatQueueActions.js';
|
||||
import { registerQuickChatActions } from './actions/chatQuickInputActions.js';
|
||||
import { ChatAgentRecommendation } from './actions/chatAgentRecommendationActions.js';
|
||||
import { registerChatTitleActions } from './actions/chatTitleActions.js';
|
||||
@@ -141,6 +145,11 @@ import { ChatTipService, IChatTipService } from './chatTipService.js';
|
||||
const toolReferenceNameEnumValues: string[] = [];
|
||||
const toolReferenceNameEnumDescriptions: string[] = [];
|
||||
|
||||
// Register JSON schema for hook files
|
||||
const jsonContributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
|
||||
jsonContributionRegistry.registerSchema(HOOK_SCHEMA_URI, hookFileSchema);
|
||||
jsonContributionRegistry.registerSchemaAssociation(HOOK_SCHEMA_URI, HOOK_FILE_GLOB);
|
||||
|
||||
// Register configuration
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
@@ -597,6 +606,12 @@ configurationRegistry.registerConfiguration({
|
||||
}
|
||||
}
|
||||
},
|
||||
[ChatConfiguration.RequestQueueingEnabled]: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('chat.requestQueuing.enabled.description', "When enabled, allows queuing additional messages while a request is in progress and steering the current request with a new message."),
|
||||
default: false,
|
||||
tags: ['experimental'],
|
||||
},
|
||||
[ChatConfiguration.EditModeHidden]: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('chat.editMode.hidden', "When enabled, hides the Edit mode from the chat mode picker."),
|
||||
@@ -891,6 +906,43 @@ configurationRegistry.registerConfiguration({
|
||||
},
|
||||
],
|
||||
},
|
||||
[PromptsConfig.HOOKS_LOCATION_KEY]: {
|
||||
type: 'object',
|
||||
title: nls.localize('chat.hookFilesLocations.title', "Hook File Locations",),
|
||||
markdownDescription: nls.localize(
|
||||
'chat.hookFilesLocations.description',
|
||||
"Specify paths to hook configuration files that define custom shell commands to execute at strategic points in an agent's workflow. [Learn More]({0}).\n\nRelative paths are resolved from the root folder(s) of your workspace. Supports Copilot hooks (`hooks.json`) and Claude Code hooks (`settings.json`, `settings.local.json`).",
|
||||
HOOK_DOCUMENTATION_URL,
|
||||
),
|
||||
default: {
|
||||
...DEFAULT_HOOK_FILE_PATHS.map((f) => ({ [f.path]: true })).reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
||||
},
|
||||
additionalProperties: { type: 'boolean' },
|
||||
propertyNames: {
|
||||
pattern: VALID_PROMPT_FOLDER_PATTERN,
|
||||
patternErrorMessage: nls.localize('chat.hookFilesLocations.invalidPath', "Paths must be relative or start with '~/'. Absolute paths and '\\' separators are not supported."),
|
||||
},
|
||||
restricted: true,
|
||||
tags: ['prompts', 'hooks', 'agent'],
|
||||
examples: [
|
||||
{
|
||||
[DEFAULT_HOOK_FILE_PATHS[0].path]: true,
|
||||
},
|
||||
{
|
||||
[DEFAULT_HOOK_FILE_PATHS[0].path]: true,
|
||||
'custom-hooks/hooks.json': true,
|
||||
},
|
||||
],
|
||||
},
|
||||
[PromptsConfig.USE_CHAT_HOOKS]: {
|
||||
type: 'boolean',
|
||||
title: nls.localize('chat.useChatHooks.title', "Use Chat Hooks",),
|
||||
markdownDescription: nls.localize('chat.useChatHooks.description', "Controls whether chat hooks are executed at strategic points during an agent's workflow. Hooks are loaded from the files configured in `#chat.hookFilesLocations#`.",),
|
||||
default: true,
|
||||
restricted: true,
|
||||
disallowConfigurationDefault: true,
|
||||
tags: ['prompts', 'hooks', 'agent']
|
||||
},
|
||||
[PromptsConfig.PROMPT_FILES_SUGGEST_KEY]: {
|
||||
type: 'object',
|
||||
scope: ConfigurationScope.RESOURCE,
|
||||
@@ -1398,6 +1450,7 @@ registerChatFileTreeActions();
|
||||
registerChatPromptNavigationActions();
|
||||
registerChatTitleActions();
|
||||
registerChatExecuteActions();
|
||||
registerChatQueueActions();
|
||||
registerQuickChatActions();
|
||||
registerChatExportActions();
|
||||
registerMoveActions();
|
||||
@@ -1433,6 +1486,7 @@ registerSingleton(ICodeMapperService, CodeMapperService, InstantiationType.Delay
|
||||
registerSingleton(IChatEditingService, ChatEditingService, InstantiationType.Delayed);
|
||||
registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, InstantiationType.Delayed);
|
||||
registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed);
|
||||
registerSingleton(IHooksExecutionService, HooksExecutionService, InstantiationType.Delayed);
|
||||
registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed);
|
||||
registerSingleton(IChatContextPickService, ChatContextPickService, InstantiationType.Delayed);
|
||||
registerSingleton(IChatModeService, ChatModeService, InstantiationType.Delayed);
|
||||
|
||||
@@ -19,8 +19,8 @@ import { IChatResponseModel, IChatModelInputState } from '../common/model/chatMo
|
||||
import { IChatMode } from '../common/chatModes.js';
|
||||
import { IParsedChatRequest } from '../common/requestParser/chatParserTypes.js';
|
||||
import { CHAT_PROVIDER_ID } from '../common/participants/chatParticipantContribTypes.js';
|
||||
import { IChatElicitationRequest, IChatLocationData, IChatSendRequestOptions } from '../common/chatService/chatService.js';
|
||||
import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel } from '../common/model/chatViewModel.js';
|
||||
import { ChatRequestQueueKind, IChatElicitationRequest, IChatLocationData, IChatSendRequestOptions } from '../common/chatService/chatService.js';
|
||||
import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatPendingDividerViewModel } from '../common/model/chatViewModel.js';
|
||||
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
|
||||
import { ChatAttachmentModel } from './attachments/chatAttachmentModel.js';
|
||||
import { IChatEditorOptions } from './widgetHosts/editor/chatEditor.js';
|
||||
@@ -207,7 +207,7 @@ export interface IChatFileTreeInfo {
|
||||
focus(): void;
|
||||
}
|
||||
|
||||
export type ChatTreeItem = IChatRequestViewModel | IChatResponseViewModel;
|
||||
export type ChatTreeItem = IChatRequestViewModel | IChatResponseViewModel | IChatPendingDividerViewModel;
|
||||
|
||||
export interface IChatListItemRendererOptions {
|
||||
readonly renderStyle?: 'compact' | 'minimal';
|
||||
@@ -306,6 +306,11 @@ export interface IChatAcceptInputOptions {
|
||||
// box's current content is being accepted, or 'false' if a specific input
|
||||
// is being submitted to the widget.
|
||||
storeToHistory?: boolean;
|
||||
/**
|
||||
* When set, queues this message to be sent after the current request completes.
|
||||
* If Steering, also sets yieldRequested on any active request to signal it should wrap up.
|
||||
*/
|
||||
queue?: ChatRequestQueueKind;
|
||||
}
|
||||
|
||||
export interface IChatWidgetViewModelChangeEvent {
|
||||
|
||||
@@ -259,11 +259,11 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
private readonly _alternativeIdMap: Map</* alternativeId */ string, /* primaryType */ string> = new Map();
|
||||
private readonly _contextKeys = new Set<string>();
|
||||
|
||||
private readonly _onDidChangeItemsProviders = this._register(new Emitter<IChatSessionItemProvider>());
|
||||
readonly onDidChangeItemsProviders: Event<IChatSessionItemProvider> = this._onDidChangeItemsProviders.event;
|
||||
private readonly _onDidChangeItemsProviders = this._register(new Emitter<{ readonly chatSessionType: string }>());
|
||||
readonly onDidChangeItemsProviders = this._onDidChangeItemsProviders.event;
|
||||
|
||||
private readonly _onDidChangeSessionItems = this._register(new Emitter<string>());
|
||||
readonly onDidChangeSessionItems: Event<string> = this._onDidChangeSessionItems.event;
|
||||
private readonly _onDidChangeSessionItems = this._register(new Emitter<{ readonly chatSessionType: string }>());
|
||||
readonly onDidChangeSessionItems = this._onDidChangeSessionItems.event;
|
||||
|
||||
private readonly _onDidChangeAvailability = this._register(new Emitter<void>());
|
||||
readonly onDidChangeAvailability: Event<void> = this._onDidChangeAvailability.event;
|
||||
@@ -339,7 +339,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this.onDidChangeSessionItems(chatSessionType => {
|
||||
this._register(this.onDidChangeSessionItems(({ chatSessionType }) => {
|
||||
this.updateInProgressStatus(chatSessionType).catch(error => {
|
||||
this._logService.warn(`Failed to update progress status for '${chatSessionType}':`, error);
|
||||
});
|
||||
@@ -637,7 +637,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
this._onDidChangeItemsProviders.fire(provider);
|
||||
}
|
||||
for (const { contribution } of this._contributions.values()) {
|
||||
this._onDidChangeSessionItems.fire(contribution.type);
|
||||
this._onDidChangeSessionItems.fire({ chatSessionType: contribution.type });
|
||||
}
|
||||
}
|
||||
this._updateHasCanDelegateProvidersContextKey();
|
||||
@@ -730,7 +730,11 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
return this._isContributionAvailable(contribution) ? contribution : undefined;
|
||||
}
|
||||
|
||||
async activateChatSessionItemProvider(chatViewType: string): Promise<IChatSessionItemProvider | undefined> {
|
||||
async activateChatSessionItemProvider(chatViewType: string): Promise<void> {
|
||||
await this.doActivateChatSessionItemProvider(chatViewType);
|
||||
}
|
||||
|
||||
private async doActivateChatSessionItemProvider(chatViewType: string): Promise<IChatSessionItemProvider | undefined> {
|
||||
await this._extensionService.whenInstalledExtensionsRegistered();
|
||||
const resolvedType = this._resolveToPrimaryType(chatViewType);
|
||||
if (resolvedType) {
|
||||
@@ -777,7 +781,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
continue; // skip: not considered for resolving
|
||||
}
|
||||
|
||||
const provider = await this.activateChatSessionItemProvider(contrib.type);
|
||||
const provider = await this.doActivateChatSessionItemProvider(contrib.type);
|
||||
if (!provider) {
|
||||
// We requested this provider but it is not available
|
||||
if (providersToResolve?.includes(contrib.type)) {
|
||||
@@ -828,7 +832,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
disposables.add(provider.onDidChangeChatSessionItems(() => {
|
||||
this._onDidChangeSessionItems.fire(chatSessionType);
|
||||
this._onDidChangeSessionItems.fire({ chatSessionType });
|
||||
}));
|
||||
|
||||
this.updateInProgressStatus(chatSessionType).catch(error => {
|
||||
@@ -1009,10 +1013,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
return !!session?.setOption(optionId, value);
|
||||
}
|
||||
|
||||
public notifySessionItemsChanged(chatSessionType: string): void {
|
||||
this._onDidChangeSessionItems.fire(chatSessionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store option groups for a session type
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChatViewId } from '../chat.js';
|
||||
import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from '../actions/chatActions.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
|
||||
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
|
||||
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
|
||||
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { formatHookCommandLabel, HOOK_TYPES, HookType } from '../../common/promptSyntax/hookSchema.js';
|
||||
import { NEW_HOOK_COMMAND_ID } from './newPromptFileActions.js';
|
||||
import { ILabelService } from '../../../../../platform/label/common/label.js';
|
||||
import { IEditorService } from '../../../../services/editor/common/editorService.js';
|
||||
import { ITextEditorSelection } from '../../../../../platform/editor/common/editor.js';
|
||||
import { findHookCommandSelection } from './hookUtils.js';
|
||||
import { getHookSourceFormatLabel, HookSourceFormat, isReadOnlyHookSource, parseHooksFromFile } from '../../common/promptSyntax/hookCompatibility.js';
|
||||
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
|
||||
import { IPathService } from '../../../../services/path/common/pathService.js';
|
||||
|
||||
/**
|
||||
* Action ID for the `Configure Hooks` action.
|
||||
*/
|
||||
const CONFIGURE_HOOKS_ACTION_ID = 'workbench.action.chat.configure.hooks';
|
||||
|
||||
interface IHookEntry {
|
||||
readonly hookType: HookType;
|
||||
readonly hookTypeLabel: string;
|
||||
/** The original hook type ID as it appears in the JSON file (for selection lookup) */
|
||||
readonly originalHookTypeId: string;
|
||||
readonly fileUri: URI;
|
||||
readonly filePath: string;
|
||||
readonly displayLabel: string;
|
||||
readonly commandFieldName: 'command' | 'bash' | 'powershell' | undefined;
|
||||
readonly index: number;
|
||||
/** The source format (Copilot, Claude) */
|
||||
readonly sourceFormat: HookSourceFormat;
|
||||
/** Whether this hook is from a read-only source (Claude settings) */
|
||||
readonly isReadOnly: boolean;
|
||||
}
|
||||
|
||||
interface IHookQuickPickItem extends IQuickPickItem {
|
||||
readonly hookEntry?: IHookEntry;
|
||||
readonly commandId?: string;
|
||||
}
|
||||
|
||||
class ManageHooksAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: CONFIGURE_HOOKS_ACTION_ID,
|
||||
title: localize2('configure-hooks', "Configure Hooks..."),
|
||||
shortTitle: localize2('configure-hooks.short', "Hooks"),
|
||||
icon: Codicon.zap,
|
||||
f1: true,
|
||||
precondition: ChatContextKeys.enabled,
|
||||
category: CHAT_CATEGORY,
|
||||
menu: {
|
||||
id: CHAT_CONFIG_MENU_ID,
|
||||
when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),
|
||||
order: 12,
|
||||
group: '1_level'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override async run(
|
||||
accessor: ServicesAccessor,
|
||||
): Promise<void> {
|
||||
const promptsService = accessor.get(IPromptsService);
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
const labelService = accessor.get(ILabelService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const workspaceService = accessor.get(IWorkspaceContextService);
|
||||
const pathService = accessor.get(IPathService);
|
||||
|
||||
// Get workspace root and user home for path resolution
|
||||
const workspaceFolder = workspaceService.getWorkspace().folders[0];
|
||||
const workspaceRootUri = workspaceFolder?.uri;
|
||||
const userHomeUri = await pathService.userHome();
|
||||
const userHome = userHomeUri.fsPath ?? userHomeUri.path;
|
||||
|
||||
// Get all hook files
|
||||
const hookFiles = await promptsService.listPromptFiles(PromptsType.hook, CancellationToken.None);
|
||||
|
||||
// Parse hook files to extract hook entries using format-aware parsing
|
||||
const hookEntries: IHookEntry[] = [];
|
||||
|
||||
for (const hookFile of hookFiles) {
|
||||
try {
|
||||
const content = await fileService.readFile(hookFile.uri);
|
||||
const json = JSON.parse(content.value.toString());
|
||||
|
||||
// Use format-aware parsing
|
||||
const { format, hooks } = parseHooksFromFile(hookFile.uri, json, workspaceRootUri, userHome);
|
||||
const isReadOnly = isReadOnlyHookSource(format);
|
||||
|
||||
for (const [hookType, { hooks: commands, originalId }] of hooks) {
|
||||
const hookTypeMeta = HOOK_TYPES.find(h => h.id === hookType);
|
||||
if (!hookTypeMeta) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const hookCommand = commands[i];
|
||||
const displayLabel = formatHookCommandLabel(hookCommand) || localize('commands.hook.emptyCommand', '(empty command)');
|
||||
hookEntries.push({
|
||||
hookType,
|
||||
hookTypeLabel: hookTypeMeta.label,
|
||||
originalHookTypeId: originalId,
|
||||
fileUri: hookFile.uri,
|
||||
filePath: labelService.getUriLabel(hookFile.uri, { relative: true }),
|
||||
displayLabel,
|
||||
commandFieldName: hookCommand.command !== undefined ? 'command' : hookCommand.bash !== undefined ? 'bash' : 'powershell',
|
||||
index: i,
|
||||
sourceFormat: format,
|
||||
isReadOnly
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Build quick pick items grouped by hook type
|
||||
const items: (IHookQuickPickItem | IQuickPickSeparator)[] = [];
|
||||
|
||||
// Add "New Hook..." option at the top
|
||||
items.push({
|
||||
label: `$(plus) ${localize('commands.new-hook.label', 'Add new hook...')}`,
|
||||
commandId: NEW_HOOK_COMMAND_ID,
|
||||
alwaysShow: true
|
||||
});
|
||||
|
||||
// Group entries by hook type
|
||||
const groupedByType = new Map<HookType, IHookEntry[]>();
|
||||
for (const entry of hookEntries) {
|
||||
const existing = groupedByType.get(entry.hookType) ?? [];
|
||||
existing.push(entry);
|
||||
groupedByType.set(entry.hookType, existing);
|
||||
}
|
||||
|
||||
// Sort hook types by their position in HOOK_TYPES
|
||||
const sortedHookTypes = Array.from(groupedByType.keys()).sort((a, b) => {
|
||||
const indexA = HOOK_TYPES.findIndex(h => h.id === a);
|
||||
const indexB = HOOK_TYPES.findIndex(h => h.id === b);
|
||||
return indexA - indexB;
|
||||
});
|
||||
|
||||
// Add entries grouped by hook type
|
||||
for (const hookTypeId of sortedHookTypes) {
|
||||
const entries = groupedByType.get(hookTypeId)!;
|
||||
const hookType = HOOK_TYPES.find(h => h.id === hookTypeId)!;
|
||||
|
||||
items.push({
|
||||
type: 'separator',
|
||||
label: hookType.label
|
||||
});
|
||||
|
||||
for (const entry of entries) {
|
||||
// Build description with source format indicator for read-only hooks
|
||||
let description = entry.filePath;
|
||||
if (entry.isReadOnly) {
|
||||
description = `$(lock) ${getHookSourceFormatLabel(entry.sourceFormat)} · ${description}`;
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: entry.displayLabel,
|
||||
description,
|
||||
hookEntry: entry
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show empty state message if no hooks found
|
||||
if (hookEntries.length === 0) {
|
||||
items.push({
|
||||
type: 'separator',
|
||||
label: localize('noHooks', "No hooks configured")
|
||||
});
|
||||
}
|
||||
|
||||
const selected = await quickInputService.pick(items, {
|
||||
placeHolder: localize('commands.hooks.placeholder', 'Select a hook to open or add a new hook'),
|
||||
title: localize('commands.hooks.title', 'Hooks')
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
if (selected.commandId) {
|
||||
await commandService.executeCommand(selected.commandId);
|
||||
} else if (selected.hookEntry) {
|
||||
const entry = selected.hookEntry;
|
||||
let selection: ITextEditorSelection | undefined;
|
||||
|
||||
// Try to find the command field to highlight
|
||||
if (entry.commandFieldName) {
|
||||
try {
|
||||
const content = await fileService.readFile(entry.fileUri);
|
||||
selection = findHookCommandSelection(
|
||||
content.value.toString(),
|
||||
entry.originalHookTypeId,
|
||||
entry.index,
|
||||
entry.commandFieldName
|
||||
);
|
||||
} catch {
|
||||
// Ignore errors and just open without selection
|
||||
}
|
||||
}
|
||||
|
||||
await editorService.openEditor({
|
||||
resource: entry.fileUri,
|
||||
options: {
|
||||
selection,
|
||||
pinned: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to register the `Manage Hooks` action.
|
||||
*/
|
||||
export function registerHookActions(): void {
|
||||
registerAction2(ManageHooksAction);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { findNodeAtLocation, Node, parseTree } from '../../../../../base/common/json.js';
|
||||
import { ITextEditorSelection } from '../../../../../platform/editor/common/editor.js';
|
||||
|
||||
/**
|
||||
* Converts an offset in content to a 1-based line and column.
|
||||
*/
|
||||
function offsetToPosition(content: string, offset: number): { line: number; column: number } {
|
||||
let line = 1;
|
||||
let column = 1;
|
||||
for (let i = 0; i < offset && i < content.length; i++) {
|
||||
if (content[i] === '\n') {
|
||||
line++;
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
}
|
||||
return { line, column };
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the n-th command field node in a hook type array, handling both simple and nested formats.
|
||||
* This iterates through the structure in the same order as the parser flattens hooks.
|
||||
*/
|
||||
function findNthCommandNode(tree: Node, hookType: string, targetIndex: number, fieldName: string): Node | undefined {
|
||||
const hookTypeArray = findNodeAtLocation(tree, ['hooks', hookType]);
|
||||
if (!hookTypeArray || hookTypeArray.type !== 'array' || !hookTypeArray.children) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
for (let i = 0; i < hookTypeArray.children.length; i++) {
|
||||
const item = hookTypeArray.children[i];
|
||||
if (item.type !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this item has nested hooks (matcher format)
|
||||
const nestedHooksNode = findNodeAtLocation(tree, ['hooks', hookType, i, 'hooks']);
|
||||
if (nestedHooksNode && nestedHooksNode.type === 'array' && nestedHooksNode.children) {
|
||||
// Iterate through nested hooks
|
||||
for (let j = 0; j < nestedHooksNode.children.length; j++) {
|
||||
if (currentIndex === targetIndex) {
|
||||
return findNodeAtLocation(tree, ['hooks', hookType, i, 'hooks', j, fieldName]);
|
||||
}
|
||||
currentIndex++;
|
||||
}
|
||||
} else {
|
||||
// Simple format - direct command
|
||||
if (currentIndex === targetIndex) {
|
||||
return findNodeAtLocation(tree, ['hooks', hookType, i, fieldName]);
|
||||
}
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the selection range for a hook command field value in JSON content.
|
||||
* Supports both simple format and nested matcher format:
|
||||
* - Simple: { hooks: { hookType: [{ command: "..." }] } }
|
||||
* - Nested: { hooks: { hookType: [{ matcher: "", hooks: [{ command: "..." }] }] } }
|
||||
*
|
||||
* The index is a flattened index across all commands in the hook type, regardless of nesting.
|
||||
*
|
||||
* @param content The JSON file content
|
||||
* @param hookType The hook type (e.g., "sessionStart")
|
||||
* @param index The flattened index of the hook command within the hook type
|
||||
* @param fieldName The field name to find ('command', 'bash', or 'powershell')
|
||||
* @returns The selection range for the field value, or undefined if not found
|
||||
*/
|
||||
export function findHookCommandSelection(content: string, hookType: string, index: number, fieldName: string): ITextEditorSelection | undefined {
|
||||
const tree = parseTree(content);
|
||||
if (!tree) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const node = findNthCommandNode(tree, hookType, index, fieldName);
|
||||
if (!node || node.type !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Node offset/length includes quotes, so adjust to select only the value content
|
||||
const valueStart = node.offset + 1; // After opening quote
|
||||
const valueEnd = node.offset + node.length - 1; // Before closing quote
|
||||
|
||||
const start = offsetToPosition(content, valueStart);
|
||||
const end = offsetToPosition(content, valueEnd);
|
||||
|
||||
return {
|
||||
startLineNumber: start.line,
|
||||
startColumn: start.column,
|
||||
endLineNumber: end.line,
|
||||
endColumn: end.column
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { isEqual } from '../../../../../base/common/resources.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { VSBuffer } from '../../../../../base/common/buffer.js';
|
||||
import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
|
||||
import { SnippetController2 } from '../../../../../editor/contrib/snippet/browser/snippetController2.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
@@ -25,7 +26,11 @@ import { CHAT_CATEGORY } from '../actions/chatActions.js';
|
||||
import { askForPromptFileName } from './pickers/askForPromptName.js';
|
||||
import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';
|
||||
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { getCleanPromptName, SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { getCleanPromptName, SKILL_FILENAME, HOOKS_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { HOOK_TYPES, HookType } from '../../common/promptSyntax/hookSchema.js';
|
||||
import { findHookCommandSelection } from './hookUtils.js';
|
||||
import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js';
|
||||
import { Range } from '../../../../../editor/common/core/range.js';
|
||||
|
||||
|
||||
class AbstractNewPromptFileAction extends Action2 {
|
||||
@@ -175,6 +180,11 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi
|
||||
`---`,
|
||||
`\${3:Define the functionality provided by this skill, including detailed instructions and examples}`,
|
||||
].join('\n');
|
||||
case PromptsType.hook:
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
hooks: {}
|
||||
}, null, 4);
|
||||
default:
|
||||
throw new Error(`Unsupported prompt type: ${promptType}`);
|
||||
}
|
||||
@@ -186,6 +196,7 @@ export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt';
|
||||
export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions';
|
||||
export const NEW_AGENT_COMMAND_ID = 'workbench.command.new.agent';
|
||||
export const NEW_SKILL_COMMAND_ID = 'workbench.command.new.skill';
|
||||
export const NEW_HOOK_COMMAND_ID = 'workbench.command.new.hook';
|
||||
|
||||
class NewPromptFileAction extends AbstractNewPromptFileAction {
|
||||
constructor() {
|
||||
@@ -288,6 +299,168 @@ class NewSkillFileAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
class NewHookFileAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: NEW_HOOK_COMMAND_ID,
|
||||
title: localize('commands.new.hook.local.title', "New Hook..."),
|
||||
f1: false,
|
||||
precondition: ChatContextKeys.enabled,
|
||||
category: CHAT_CATEGORY,
|
||||
keybinding: {
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
},
|
||||
menu: {
|
||||
id: MenuId.CommandPalette,
|
||||
when: ChatContextKeys.enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override async run(accessor: ServicesAccessor) {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
const bulkEditService = accessor.get(IBulkEditService);
|
||||
|
||||
const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, PromptsType.hook);
|
||||
if (!selectedFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask which hook type to add
|
||||
const hookTypeItems = HOOK_TYPES.map(hookType => ({
|
||||
id: hookType.id,
|
||||
label: hookType.label,
|
||||
description: hookType.description
|
||||
}));
|
||||
|
||||
const selectedHookType = await quickInputService.pick(hookTypeItems, {
|
||||
placeHolder: localize('commands.new.hook.type.placeholder', "Select a hook type to add"),
|
||||
title: localize('commands.new.hook.type.title', "Add Hook")
|
||||
});
|
||||
|
||||
if (!selectedHookType) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the hooks folder if it doesn't exist
|
||||
await fileService.createFolder(selectedFolder.uri);
|
||||
|
||||
// Use fixed hooks.json filename
|
||||
const hookFileUri = URI.joinPath(selectedFolder.uri, HOOKS_FILENAME);
|
||||
|
||||
// Check if hooks.json already exists
|
||||
let hooksContent: { version: number; hooks: Record<string, unknown[]> };
|
||||
const fileExists = await fileService.exists(hookFileUri);
|
||||
|
||||
if (fileExists) {
|
||||
// Parse existing file
|
||||
const existingContent = await fileService.readFile(hookFileUri);
|
||||
try {
|
||||
hooksContent = JSON.parse(existingContent.value.toString());
|
||||
// Ensure hooks object exists
|
||||
if (!hooksContent.hooks) {
|
||||
hooksContent.hooks = {};
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, show error and open file for user to fix
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
notificationService.error(localize('commands.new.hook.parseError', "Failed to parse existing hooks.json. Please fix the JSON syntax errors and try again."));
|
||||
await editorService.openEditor({ resource: hookFileUri });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Create new structure
|
||||
hooksContent = { version: 1, hooks: {} };
|
||||
}
|
||||
|
||||
// Add the new hook entry (append if hook type already exists)
|
||||
const hookTypeId = selectedHookType.id as HookType;
|
||||
const newHookEntry = {
|
||||
type: 'command',
|
||||
command: ''
|
||||
};
|
||||
let newHookIndex: number;
|
||||
if (!hooksContent.hooks[hookTypeId]) {
|
||||
hooksContent.hooks[hookTypeId] = [newHookEntry];
|
||||
newHookIndex = 0;
|
||||
} else {
|
||||
hooksContent.hooks[hookTypeId].push(newHookEntry);
|
||||
newHookIndex = hooksContent.hooks[hookTypeId].length - 1;
|
||||
}
|
||||
|
||||
// Write the file
|
||||
const jsonContent = JSON.stringify(hooksContent, null, '\t');
|
||||
|
||||
// Check if the file is already open in an editor
|
||||
const existingEditor = editorService.editors.find(e => isEqual(e.resource, hookFileUri));
|
||||
|
||||
if (existingEditor) {
|
||||
// File is already open - first focus the editor, then update its model directly
|
||||
await editorService.openEditor({
|
||||
resource: hookFileUri,
|
||||
options: {
|
||||
pinned: false
|
||||
}
|
||||
});
|
||||
|
||||
// Get the code editor and update its content directly
|
||||
const editor = getCodeEditor(editorService.activeTextEditorControl);
|
||||
if (editor && editor.hasModel() && isEqual(editor.getModel().uri, hookFileUri)) {
|
||||
const model = editor.getModel();
|
||||
// Apply the full content replacement using executeEdits
|
||||
model.pushEditOperations([], [{
|
||||
range: model.getFullModelRange(),
|
||||
text: jsonContent
|
||||
}], () => null);
|
||||
|
||||
// Find and apply the selection
|
||||
const selection = findHookCommandSelection(jsonContent, hookTypeId, newHookIndex, 'command');
|
||||
if (selection && selection.endLineNumber !== undefined && selection.endColumn !== undefined) {
|
||||
editor.setSelection({
|
||||
startLineNumber: selection.startLineNumber,
|
||||
startColumn: selection.startColumn,
|
||||
endLineNumber: selection.endLineNumber,
|
||||
endColumn: selection.endColumn
|
||||
});
|
||||
editor.revealLineInCenter(selection.startLineNumber);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// File is not currently open in an editor
|
||||
if (!fileExists) {
|
||||
// File doesn't exist - write new file directly and open
|
||||
await fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent));
|
||||
} else {
|
||||
// File exists but isn't open - open it first, then use bulk edit for undo support
|
||||
await editorService.openEditor({
|
||||
resource: hookFileUri,
|
||||
options: { pinned: false }
|
||||
});
|
||||
|
||||
// Apply the edit via bulk edit service for proper undo support
|
||||
await bulkEditService.apply([
|
||||
new ResourceTextEdit(hookFileUri, { range: new Range(1, 1, Number.MAX_SAFE_INTEGER, 1), text: jsonContent })
|
||||
], { label: localize('addHook', "Add Hook") });
|
||||
}
|
||||
|
||||
// Find the selection for the new hook's command field
|
||||
const selection = findHookCommandSelection(jsonContent, hookTypeId, newHookIndex, 'command');
|
||||
|
||||
// Open editor with selection (or re-focus if already open)
|
||||
await editorService.openEditor({
|
||||
resource: hookFileUri,
|
||||
options: {
|
||||
selection,
|
||||
pinned: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NewUntitledPromptFileAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -333,5 +506,6 @@ export function registerNewPromptFileActions(): void {
|
||||
registerAction2(NewInstructionsFileAction);
|
||||
registerAction2(NewAgentFileAction);
|
||||
registerAction2(NewSkillFileAction);
|
||||
registerAction2(NewHookFileAction);
|
||||
registerAction2(NewUntitledPromptFileAction);
|
||||
}
|
||||
|
||||
+10
@@ -113,6 +113,8 @@ function getPlaceholderStringforNew(type: PromptsType): string {
|
||||
return localize('workbench.command.agent.create.location.placeholder', "Select a location to create the agent file");
|
||||
case PromptsType.skill:
|
||||
return localize('workbench.command.skill.create.location.placeholder', "Select a location to create the skill");
|
||||
case PromptsType.hook:
|
||||
return localize('workbench.command.hook.create.location.placeholder', "Select a location to create the hook file");
|
||||
default:
|
||||
throw new Error('Unknown prompt type');
|
||||
}
|
||||
@@ -129,6 +131,8 @@ function getPlaceholderStringforMove(type: PromptsType, isMove: boolean): string
|
||||
return localize('agent.move.location.placeholder', "Select a location to move the agent file to");
|
||||
case PromptsType.skill:
|
||||
return localize('skill.move.location.placeholder', "Select a location to move the skill to");
|
||||
case PromptsType.hook:
|
||||
throw new Error('Hooks cannot be moved');
|
||||
default:
|
||||
throw new Error('Unknown prompt type');
|
||||
}
|
||||
@@ -142,6 +146,8 @@ function getPlaceholderStringforMove(type: PromptsType, isMove: boolean): string
|
||||
return localize('agent.copy.location.placeholder', "Select a location to copy the agent file to");
|
||||
case PromptsType.skill:
|
||||
return localize('skill.copy.location.placeholder', "Select a location to copy the skill to");
|
||||
case PromptsType.hook:
|
||||
throw new Error('Hooks cannot be copied');
|
||||
default:
|
||||
throw new Error('Unknown prompt type');
|
||||
}
|
||||
@@ -187,6 +193,8 @@ function getLearnLabel(type: PromptsType): string {
|
||||
return localize('commands.agent.create.ask-folder.empty.docs-label', 'Learn how to configure custom agents');
|
||||
case PromptsType.skill:
|
||||
return localize('commands.skill.create.ask-folder.empty.docs-label', 'Learn how to configure skills');
|
||||
case PromptsType.hook:
|
||||
return localize('commands.hook.create.ask-folder.empty.docs-label', 'Learn how to configure hooks');
|
||||
default:
|
||||
throw new Error('Unknown prompt type');
|
||||
}
|
||||
@@ -202,6 +210,8 @@ function getMissingSourceFolderString(type: PromptsType): string {
|
||||
return localize('commands.agent.create.ask-folder.empty.placeholder', 'No agent source folders found.');
|
||||
case PromptsType.skill:
|
||||
return localize('commands.skill.create.ask-folder.empty.placeholder', 'No skill source folders found.');
|
||||
case PromptsType.hook:
|
||||
return localize('commands.hook.create.ask-folder.empty.placeholder', 'No hook source folders found.');
|
||||
default:
|
||||
throw new Error('Unknown prompt type');
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { IOpenerService } from '../../../../../../platform/opener/common/opener.
|
||||
import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { ICommandService } from '../../../../../../platform/commands/common/commands.js';
|
||||
import { getCleanPromptName } from '../../../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { PromptsType, INSTRUCTIONS_DOCUMENTATION_URL, AGENT_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL } from '../../../common/promptSyntax/promptTypes.js';
|
||||
import { PromptsType, INSTRUCTIONS_DOCUMENTATION_URL, AGENT_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL, HOOK_DOCUMENTATION_URL } from '../../../common/promptSyntax/promptTypes.js';
|
||||
import { NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../newPromptFileActions.js';
|
||||
import { IKeyMods, IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from '../../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { askForPromptFileName } from './askForPromptName.js';
|
||||
@@ -98,6 +98,12 @@ function newHelpButton(type: PromptsType): IQuickInputButton & { helpURI: URI }
|
||||
helpURI: URI.parse(SKILL_DOCUMENTATION_URL),
|
||||
iconClass
|
||||
};
|
||||
case PromptsType.hook:
|
||||
return {
|
||||
tooltip: localize('help.hook', "Show help on hook files"),
|
||||
helpURI: URI.parse(HOOK_DOCUMENTATION_URL),
|
||||
iconClass
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { registerAgentActions } from './chatModeActions.js';
|
||||
import { registerRunPromptActions } from './runPromptAction.js';
|
||||
import { registerNewPromptFileActions } from './newPromptFileActions.js';
|
||||
import { registerSkillActions } from './skillActions.js';
|
||||
import { registerHookActions } from './hookActions.js';
|
||||
import { registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { SaveAsAgentFileAction, SaveAsInstructionsFileAction, SaveAsPromptFileAction } from './saveAsPromptFileActions.js';
|
||||
|
||||
@@ -19,6 +20,7 @@ export function registerPromptActions(): void {
|
||||
registerRunPromptActions();
|
||||
registerAttachPromptActions();
|
||||
registerSkillActions();
|
||||
registerHookActions();
|
||||
registerAction2(SaveAsPromptFileAction);
|
||||
registerAction2(SaveAsInstructionsFileAction);
|
||||
registerAction2(SaveAsAgentFileAction);
|
||||
|
||||
+3
-2
@@ -7,7 +7,7 @@ import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IChatProgressRenderableResponseContent } from '../../../common/model/chatModel.js';
|
||||
import { IChatConfirmation, IChatSendRequestOptions, IChatService } from '../../../common/chatService/chatService.js';
|
||||
import { ChatSendResult, IChatConfirmation, IChatSendRequestOptions, IChatService } from '../../../common/chatService/chatService.js';
|
||||
import { isResponseVM } from '../../../common/model/chatViewModel.js';
|
||||
import { IChatWidgetService } from '../../chat.js';
|
||||
import { SimpleChatConfirmationWidget } from './chatConfirmationWidget.js';
|
||||
@@ -54,7 +54,8 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont
|
||||
options.location = widget?.location;
|
||||
Object.assign(options, widget?.getModeRequestOptions());
|
||||
|
||||
if (await this.chatService.sendRequest(element.sessionResource, prompt, options)) {
|
||||
const result = await this.chatService.sendRequest(element.sessionResource, prompt, options);
|
||||
if (ChatSendResult.isSent(result)) {
|
||||
confirmation.isUsed = true;
|
||||
confirmationWidget.setShowButtons(false);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { ChatTreeItem, IChatCodeBlockInfo } from '../../chat.js';
|
||||
import { IChatRendererContent } from '../../../common/model/chatViewModel.js';
|
||||
import { IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel } from '../../../common/model/chatViewModel.js';
|
||||
import { CodeBlockModelCollection } from '../../../common/widget/codeBlockModelCollection.js';
|
||||
import { DiffEditorPool, EditorPool } from './chatContentCodePools.js';
|
||||
import { IObservable } from '../../../../../../base/common/observable.js';
|
||||
@@ -41,7 +41,7 @@ export interface IChatContentPart extends IDisposable {
|
||||
}
|
||||
|
||||
export interface IChatContentPartRenderContext {
|
||||
readonly element: ChatTreeItem;
|
||||
readonly element: IChatRequestViewModel | IChatResponseViewModel;
|
||||
readonly elementIndex: number;
|
||||
readonly container: HTMLElement;
|
||||
readonly content: ReadonlyArray<IChatRendererContent>;
|
||||
|
||||
+12
@@ -5,6 +5,8 @@
|
||||
|
||||
import * as dom from '../../../../../../base/browser/dom.js';
|
||||
import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js';
|
||||
import { getBaseLayerHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegate2.js';
|
||||
import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
|
||||
import { Emitter, Event } from '../../../../../../base/common/event.js';
|
||||
import { KeyCode } from '../../../../../../base/common/keyCodes.js';
|
||||
import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
@@ -161,6 +163,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
e.stopPropagation();
|
||||
this.handleNext();
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && (event.keyCode === KeyCode.Backspace || event.keyCode === KeyCode.Delete)) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -377,6 +381,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
? questionText
|
||||
: questionText.value;
|
||||
|
||||
title.setAttribute('aria-label', messageContent);
|
||||
|
||||
// Check for subtitle in parentheses at the end
|
||||
const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/);
|
||||
if (parenMatch) {
|
||||
@@ -547,6 +553,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
const listItem = dom.$('.chat-question-list-item');
|
||||
listItem.setAttribute('role', 'option');
|
||||
listItem.setAttribute('aria-selected', String(isSelected));
|
||||
listItem.setAttribute('aria-label', localize('chat.questionCarousel.optionLabel', "Option {0}: {1}", index + 1, option.label));
|
||||
listItem.id = `option-${question.id}-${index}`;
|
||||
listItem.tabIndex = -1;
|
||||
|
||||
@@ -582,6 +589,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
listItem.classList.add('selected');
|
||||
}
|
||||
|
||||
this._inputBoxes.add(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), listItem, option.label));
|
||||
|
||||
// Click handler
|
||||
this._inputBoxes.add(dom.addDisposableListener(listItem, dom.EventType.CLICK, (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -727,6 +736,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
const listItem = dom.$('.chat-question-list-item.multi-select');
|
||||
listItem.setAttribute('role', 'option');
|
||||
listItem.setAttribute('aria-selected', String(isChecked));
|
||||
listItem.setAttribute('aria-label', localize('chat.questionCarousel.optionLabel', "Option {0}: {1}", index + 1, option.label));
|
||||
listItem.id = `option-${question.id}-${index}`;
|
||||
listItem.tabIndex = -1;
|
||||
|
||||
@@ -781,6 +791,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
}
|
||||
}));
|
||||
|
||||
this._inputBoxes.add(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), listItem, option.label));
|
||||
|
||||
selectContainer.appendChild(listItem);
|
||||
checkboxes.push(checkbox);
|
||||
listItems.push(listItem);
|
||||
|
||||
@@ -199,6 +199,7 @@ export class ChatTodoListWidget extends Disposable {
|
||||
private createClearButton(): void {
|
||||
this.clearButton = new Button(this.clearButtonContainer, {
|
||||
supportIcons: true,
|
||||
ariaLabel: localize('chat.todoList.clearButton', 'Clear all todos'),
|
||||
});
|
||||
this.clearButton.element.tabIndex = 0;
|
||||
this.clearButton.icon = Codicon.clearAll;
|
||||
|
||||
+9
-2
@@ -239,19 +239,26 @@
|
||||
}
|
||||
|
||||
.interactive-session .interactive-response .value {
|
||||
.chat-question-list-item:focus,
|
||||
.chat-question-list-item:focus:not(.selected),
|
||||
.chat-question-list:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Single-select: highlight entire row when selected and also outline */
|
||||
/* Single-select: highlight entire row when selected */
|
||||
.chat-question-list-item.selected {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
}
|
||||
|
||||
.chat-question-list:focus-within .chat-question-list-item.selected {
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
outline-offset: -1px;
|
||||
outline-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.chat-question-list-item.selected:hover {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user