Merge origin/main into connor4312/344445

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Connor PeetandCopilot committed 2026-10-27 11:51:27 -07:00
commit 8d9e4aaa8b
559 files changed
+10095 -35715

No files matched your search

@@ -3,6 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isAzurePipelinesDefinition } from '../detect-changes/detect.ts';
interface IChangedFile {
readonly filename: string;
readonly previous_filename?: string;
@@ -70,13 +72,17 @@ const fullProfilePrefixes = [
];
function requiresFullProfile(path: string): boolean {
// Only the product builds run the Azure Pipelines definitions under build/.
if (isAzurePipelinesDefinition(path)) {
return false;
}
return fullProfilePaths.has(path)
|| fullProfilePrefixes.some(prefix => path.startsWith(prefix))
|| /(^|\/)(package(-lock)?\.json|npm-shrinkwrap\.json)$/.test(path);
}
function affectsAgentHostE2E(path: string): boolean {
if (path.endsWith('.md')) {
if (path.endsWith('.md') || isAzurePipelinesDefinition(path)) {
return false;
}
return exactPaths.has(path) || pathPrefixes.some(prefix => path.startsWith(prefix));
@@ -25,6 +25,12 @@ outputs:
product-tests:
description: "'true' when the Electron integration, Electron smoke and remote tests need to run."
value: ${{ steps.detect.outputs.product-tests }}
monaco-editor:
description: "'true' when the Monaco Editor checks need to run."
value: ${{ steps.detect.outputs.monaco-editor }}
telemetry:
description: "'true' when the telemetry extraction check needs to run."
value: ${{ steps.detect.outputs.telemetry }}
runs:
using: composite
+20 -3
View File
@@ -114,6 +114,14 @@ function isRustCrate(path: string): boolean {
return path.startsWith('cli/') || path.startsWith('copilot_shim/');
}
/**
* The Azure Pipelines definitions, which only the product builds run. In pull requests, only the build script tests of
* Compile & Hygiene read them. The scripts next to them are not definitions: some of them run in the pull request jobs.
*/
export function isAzurePipelinesDefinition(path: string): boolean {
return path.startsWith('build/azure-pipelines/') && /\.ya?ml$/.test(path);
}
/**
* Built-in extensions whose files the core unit tests read: the component fixtures load the themes and
* these grammars. build/lib/test/detectChanges.test.ts fails when a test starts reading another extension.
@@ -129,17 +137,26 @@ const coreTestRules: readonly ICoreTestRule[] = [
{
// Electron, node.js and browser unit tests load only the core sources and their test fixtures.
output: 'unit-tests',
canSkip: path => isDocumentation(path) || isRustCrate(path) || isExtensionNotReadByUnitTests(path),
canSkip: path => isDocumentation(path) || isRustCrate(path) || isAzurePipelinesDefinition(path) || isExtensionNotReadByUnitTests(path),
},
{
// The browser integration and smoke tests do not load the Copilot extension.
output: 'browser-integration',
canSkip: path => isDocumentation(path) || isRustCrate(path) || path.startsWith('extensions/copilot/'),
canSkip: path => isDocumentation(path) || isRustCrate(path) || isAzurePipelinesDefinition(path) || path.startsWith('extensions/copilot/'),
},
{
// The Electron integration, Electron smoke and remote tests exercise the built-in extensions.
output: 'product-tests',
canSkip: path => isDocumentation(path) || isRustCrate(path),
canSkip: path => isDocumentation(path) || isRustCrate(path) || isAzurePipelinesDefinition(path),
},
{
// The Monaco Editor checks also package Markdown files, such as build/monaco/README-npm.md.
output: 'monaco-editor',
canSkip: isAzurePipelinesDefinition,
},
{
output: 'telemetry',
canSkip: isAzurePipelinesDefinition,
},
];
+2 -2
View File
@@ -78,9 +78,9 @@ Cookies/login/storage → **sessions**. "Which pages can this client see" → **
## Remote pages
A browser context (its storage) is bound to a **network**: local, or remote by authority. Tabs save their requested session (storage scope, affinity and network); creation applies current trust and proxy restrictions without overwriting that request. Popups and automation contexts inherit their parent's network. Remote traffic uses an HTTPS proxy in the window's local extension host; proxy startup failure fails the view rather than falling back locally.
A browser context (its storage) is bound to a **network**: local, or remote by authority: the window's remote (remote workspace windows) or a remote agent host (Agents window sessions). Tabs save their requested session (storage scope, affinity and network); creation applies current trust and proxy restrictions without overwriting that request. Popups and automation contexts inherit their parent's network. Remote traffic uses an HTTPS proxy in the window's local extension host; proxy startup failure fails the view rather than falling back locally. A host known not to support proxying leaves new tabs on the local network with a visible warning.
The network belongs to the browser context, not its owner, audience, or policy. Global storage is always local. Workspace storage uses the window's network; other networks require a storage **affinity** selecting a partition within the workspace's storage folder, or use per-view in-memory storage without one. Clearing workspace storage includes these partitions.
The network belongs to the browser context, not its owner, audience, or policy. Global storage is always local. Workspace storage uses the window's network; other networks require a storage **affinity** (an agent host plus folder roots) selecting a partition within the workspace's storage folder, or use per-view in-memory storage without one. Clearing workspace storage includes these partitions.
## Testing strategy
+2 -1
View File
@@ -100,11 +100,12 @@ Override the default Mocha timeout for long-running tests.
#### `--workers <count>` - Run all test files in parallel processes
Spreads a run of **all** test files across `<count>` worker processes. The count is required, and capped at one worker per CPU and per 2 GB of memory, because every worker loads all test files. Every worker loads all test files and runs the files it claims, in the usual order. It is ignored, and the run uses a single window or process, with `--run`, `--runList`, `--runGlob`, bare file paths, `--grep`, `--coverage` or `--dev`. With `--rerun-failed-files`, the failures of all workers are rerun in a single window or process. When a worker fails, the runner prints the test files it ran, in order, to pass to `--run` to reproduce the failure. `npm run test-node` accepts it as well.
Spreads a run of **all** test files across `<count>` worker processes. The count is required, and capped at one worker per CPU and per 2 GB of memory, because every worker loads all test files. Every worker loads all test files and runs the files it claims, in the usual order. It is ignored, and the run uses a single window or process, with `--run`, `--runList`, `--runGlob`, bare file paths, `--grep`, `--coverage` or `--dev`. With `--rerun-failed-files`, the failures of all workers are rerun in a single window or process. When a worker fails, the runner prints the test files it ran, in order, to pass to `--run` to reproduce the failure. `npm run test-node` and `npm run test-browser` accept it as well; the browser runner starts its own browser in every worker, runs the browsers one at a time, and ignores it with `--debug`.
```bash
./scripts/test.sh --workers 4
npm run test-node -- --workers 4
npm run test-browser -- --browser chromium --workers 4
```
### Integration tests
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-24.04, macos-26, windows-2025]
steps:
- name: Checkout
+9 -9
View File
@@ -193,7 +193,7 @@ jobs:
"
- name: Upload build output
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: build-output
path: |
@@ -250,7 +250,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download build output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: build-output
@@ -356,7 +356,7 @@ jobs:
- name: Upload perf results
if: always() && steps.scenarios.outputs.skip != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: perf-results-${{ matrix.group }}
include-hidden-files: true
@@ -367,7 +367,7 @@ jobs:
- name: Upload perf summary data
if: always() && steps.scenarios.outputs.skip != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: perf-summary-${{ matrix.group }}
include-hidden-files: true
@@ -432,7 +432,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download build output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: build-output
@@ -476,7 +476,7 @@ jobs:
- name: Upload leak results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: leak-results
include-hidden-files: true
@@ -512,14 +512,14 @@ jobs:
node-version-file: .nvmrc
- name: Download perf summary data
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: perf-summary-*
path: perf-results
- name: Download leak results
if: inputs.skip_leak_check != true && needs.leak-check.result != 'skipped'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: leak-results
path: leak-results
@@ -543,7 +543,7 @@ jobs:
- name: Upload CI summary
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: chat-perf-summary
path: ci-summary.md
+3 -3
View File
@@ -79,7 +79,7 @@ jobs:
- name: Upload Playwright test results
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: playwright-test-results
path: test/componentFixtures/playwright/test-results/
@@ -141,7 +141,7 @@ jobs:
COMPONENT_EXPLORER_STATIC_BUILD: 1
- name: Upload component explorer
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: component-explorer
path: .build/rspack-serve-out/
@@ -213,7 +213,7 @@ jobs:
}
- name: Upload screenshots as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
with:
name: screenshots
+2 -2
View File
@@ -223,14 +223,14 @@ jobs:
- name: Upload serve-out log
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: serve-out-log
path: /tmp/serve-out.log
- name: Upload report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: css-order-report
path: test/componentFixtures/.build/css-order-report/
+5 -5
View File
@@ -179,7 +179,7 @@ jobs:
# steps below run every test. A pattern, unlike a name, downloads nothing instead of
# failing when there is no plan.
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: test-plan
path: .build/test-plan
@@ -205,7 +205,7 @@ jobs:
- name: 🧪 Run unit tests (Browser, Webkit)
if: ${{ env.BROWSER_TESTS == 'true' && env.UNIT_TESTS == 'true' && (hashFiles('.build/test-plan/plan.json') == '' || hashFiles('.build/test-plan/unit.txt') != '') }}
timeout-minutes: 30
run: npm run test-browser-no-install -- --browser webkit --tfs "Browser Unit Tests" --rerun-failed-files 2 ${{ hashFiles('.build/test-plan/plan.json') != '' && '--runList .build/test-plan/unit.txt' || '' }}
run: npm run test-browser-no-install -- --browser webkit --tfs "Browser Unit Tests" --workers 4 --rerun-failed-files 2 ${{ hashFiles('.build/test-plan/plan.json') != '' && '--runList .build/test-plan/unit.txt' || '' }}
env:
DEBUG: "*browser*"
@@ -331,7 +331,7 @@ jobs:
continue-on-error: true
- name: Publish Crash Reports
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure()
continue-on-error: true
with:
@@ -342,7 +342,7 @@ jobs:
# In order to properly symbolify above crash reports
# (if any), we need the compiled native modules too
- name: Publish Node Modules
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure()
continue-on-error: true
with:
@@ -351,7 +351,7 @@ jobs:
if-no-files-found: ignore
- name: Publish Log Files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
continue-on-error: true
with:
+4 -4
View File
@@ -336,7 +336,7 @@ jobs:
- name: 🧪 Run unit tests (Browser, Chromium)
if: ${{ env.BROWSER_TESTS == 'true' && env.UNIT_TESTS == 'true' }}
timeout-minutes: 30
run: npm run test-browser-no-install -- --browser chromium --tfs "Browser Unit Tests" --rerun-failed-files 2
run: npm run test-browser-no-install -- --browser chromium --tfs "Browser Unit Tests" --workers 4 --rerun-failed-files 2
env:
DEBUG: "*browser*"
@@ -493,7 +493,7 @@ jobs:
continue-on-error: true
- name: Publish Crash Reports
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure()
continue-on-error: true
with:
@@ -504,7 +504,7 @@ jobs:
# In order to properly symbolify above crash reports
# (if any), we need the compiled native modules too
- name: Publish Node Modules
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure()
continue-on-error: true
with:
@@ -513,7 +513,7 @@ jobs:
if-no-files-found: ignore
- name: Publish Log Files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
continue-on-error: true
with:
+5 -5
View File
@@ -247,7 +247,7 @@ jobs:
# steps below run every test. A pattern, unlike a name, downloads nothing instead of
# failing when there is no plan.
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: test-plan
path: .build/test-plan
@@ -277,7 +277,7 @@ jobs:
if: ${{ env.BROWSER_TESTS == 'true' && env.UNIT_TESTS == 'true' && (hashFiles('.build/test-plan/plan.json') == '' || hashFiles('.build/test-plan/unit.txt') != '') }}
timeout-minutes: 25
shell: pwsh
run: node test/unit/browser/index.js --browser chromium --tfs "Browser Unit Tests" --rerun-failed-files 2 ${{ hashFiles('.build/test-plan/plan.json') != '' && '--runList .build/test-plan/unit.txt' || '' }}
run: node test/unit/browser/index.js --browser chromium --tfs "Browser Unit Tests" --workers 4 --rerun-failed-files 2 ${{ hashFiles('.build/test-plan/plan.json') != '' && '--runList .build/test-plan/unit.txt' || '' }}
env:
DEBUG: "*browser*"
@@ -427,7 +427,7 @@ jobs:
continue-on-error: true
- name: Publish Crash Reports
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure()
continue-on-error: true
with:
@@ -438,7 +438,7 @@ jobs:
# In order to properly symbolify above crash reports
# (if any), we need the compiled native modules too
- name: Publish Node Modules
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure()
continue-on-error: true
with:
@@ -447,7 +447,7 @@ jobs:
if-no-files-found: ignore
- name: Publish Log Files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
continue-on-error: true
with:
+11 -7
View File
@@ -43,6 +43,8 @@ jobs:
unit-tests: ${{ steps.detect.outputs.unit-tests }}
browser-integration: ${{ steps.detect.outputs.browser-integration }}
product-tests: ${{ steps.detect.outputs.product-tests }}
monaco-editor: ${{ steps.detect.outputs.monaco-editor }}
telemetry: ${{ steps.detect.outputs.telemetry }}
agent-host-e2e: ${{ steps.detect-agent-host-e2e.outputs.affected }}
agent-host-test-profile: ${{ steps.detect-agent-host-e2e.outputs.profile }}
agent-host-test-profile-reason: ${{ steps.detect-agent-host-e2e.outputs.reason }}
@@ -121,7 +123,7 @@ jobs:
- name: Upload test plan
if: steps.plan.outputs.select == 'true'
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: test-plan
path: .build/test-plan
@@ -202,6 +204,8 @@ jobs:
} >> "$GITHUB_OUTPUT"
# Keep these checks first after Detect Changes so they appear at the top of the job group.
# Compile & Hygiene always runs: its build script tests also check the Azure Pipelines
# definitions, which no other job of this workflow reads.
compile:
name: Compile & Hygiene
needs: changes
@@ -314,7 +318,7 @@ jobs:
monaco-editor:
name: Monaco Editor
needs: changes
if: ${{ !cancelled() }}
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.monaco-editor == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
permissions: {}
@@ -399,7 +403,7 @@ jobs:
telemetry:
name: Telemetry
needs: changes
if: ${{ !cancelled() }}
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.telemetry == 'true') }}
runs-on: ubuntu-latest
permissions: {}
steps:
@@ -679,7 +683,7 @@ jobs:
- name: Upload simulation output
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: copilot-simulation-output-linux-${{ github.run_attempt }}
path: extensions/copilot/.simulation-archive/simulation.tgz
@@ -763,7 +767,7 @@ jobs:
name: chat-lib - Test (Linux)
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.copilot == 'true') }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
@@ -778,7 +782,7 @@ jobs:
name: chat-lib - Test (macOS)
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.copilot == 'true') }}
runs-on: macos-latest
runs-on: macos-26
timeout-minutes: 30
permissions:
contents: read
@@ -793,7 +797,7 @@ jobs:
name: chat-lib - Test (Windows)
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.copilot == 'true') }}
runs-on: windows-latest
runs-on: windows-2025
timeout-minutes: 30
permissions:
contents: read
+1 -1
View File
@@ -62,7 +62,7 @@ jobs:
- name: Upload failure screenshots
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: sessions-e2e-failures
path: src/vs/sessions/test/e2e/out/failure-*.png
+1 -1
View File
@@ -7,7 +7,7 @@
{
"kind": 2,
"language": "github-issues",
"value": "$MILESTONE=milestone:\"1.143.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified"
"value": "$MILESTONE=milestone:\"1.144.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified"
},
{
"kind": 1,
+1 -1
View File
@@ -7,7 +7,7 @@
{
"kind": 2,
"language": "github-issues",
"value": "$MILESTONE=milestone:\"1.143.0\"\n\n$MINE=assignee:@me"
"value": "$MILESTONE=milestone:\"1.144.0\"\n\n$MINE=assignee:@me"
},
{
"kind": 2,
+9 -2
View File
@@ -67,7 +67,11 @@ steps:
displayName: Install sccache
- ${{ if parameters.VSCODE_CHECK_ONLY }}:
- script: cargo clippy --config http.multiplexing=false --config net.retry=5 --target ${{ parameters.VSCODE_CLI_TARGET }} --bin=code
# clippy occasionally hangs on Windows until the job times out (#344111). A
# step timeout would cancel the step without retrying it, so the script
# fails itself after 10 min (the step takes ~1 min) and resets sccache,
# which lets the step's retry run.
- script: node ../build/azure-pipelines/common/runWithTimeout.ts 600 --on-timeout "sccache --stop-server" -- cargo clippy --config http.multiplexing=false --config net.retry=5 --target ${{ parameters.VSCODE_CLI_TARGET }} --bin=code
displayName: Lint ${{ parameters.VSCODE_CLI_TARGET }}
retryCountOnTaskFailure: 2
workingDirectory: $(Build.SourcesDirectory)/cli
@@ -149,7 +153,10 @@ steps:
${{ pair.key }}: ${{ pair.value }}
- ${{ else }}:
- script: cargo build --config http.multiplexing=false --config net.retry=5 --release --target ${{ parameters.VSCODE_CLI_TARGET }} --bin=code
# cargo build has also hung on Windows until the job timed out, failing the 11:00 insider
# 489779 (#344111). As for clippy, the script fails itself after 20 min (the step takes
# 1.5 to 4 min) and resets sccache, so the step's retry runs instead of a 90 min job timeout.
- script: node ../build/azure-pipelines/common/runWithTimeout.ts 1200 --on-timeout "sccache --stop-server" -- cargo build --config http.multiplexing=false --config net.retry=5 --release --target ${{ parameters.VSCODE_CLI_TARGET }} --bin=code
displayName: Compile ${{ parameters.VSCODE_CLI_TARGET }}
retryCountOnTaskFailure: 2
workingDirectory: $(Build.SourcesDirectory)/cli
@@ -0,0 +1,14 @@
# Restores the browsers that `npm run playwright-install` downloads, keyed on the
# pinned Playwright version, so a Playwright CDN outage doesn't fail the test
# jobs. On a hit, the install finds the browsers and downloads nothing. On a
# miss, it downloads them as before and the cache saves them after the job.
steps:
- script: node -e "console.log('##vso[task.setvariable variable=PLAYWRIGHT_BROWSERS_PATH]' + require('path').join(process.env.BUILD_SOURCESDIRECTORY, '.build', 'ms-playwright'))"
displayName: Set Playwright browsers path
- task: Cache@2
inputs:
key: '"playwright" | "$(Agent.OS)" | "$(VSCODE_ARCH)" | node_modules/playwright/package.json'
path: .build/ms-playwright
displayName: Restore Playwright browsers cache
retryCountOnTaskFailure: 2
@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Runs a command and fails it, with a non-zero exit code, when it runs longer
// than a deadline.
//
// node runWithTimeout.ts <seconds> [--on-timeout <command>] -- <command> [args...]
//
// A step's `timeoutInMinutes` cancels the step, and a cancelled step is never
// retried by `retryCountOnTaskFailure`. Failing from inside the script lets the
// step's retry run instead. `--on-timeout` runs a shell command after the
// command's process tree is killed, for example to reset a stuck daemon before
// the retry. It gets at most a minute, and its result doesn't change the exit
// code.
import { spawn, spawnSync } from 'node:child_process';
export const TIMEOUT_EXIT_CODE = 124;
/** How long the `--on-timeout` command may run, since it may talk to the stuck process. */
const ON_TIMEOUT_DEADLINE_MS = Number(process.env.RUN_WITH_TIMEOUT_CLEANUP_MS) || 60_000;
interface Options {
readonly seconds: number;
readonly onTimeout: string | undefined;
readonly command: string;
readonly args: string[];
}
export function parseArgs(argv: readonly string[]): Options {
const separator = argv.indexOf('--');
if (separator === -1 || separator === argv.length - 1) {
throw new Error('usage: runWithTimeout.ts <seconds> [--on-timeout <command>] -- <command> [args...]');
}
const head = argv.slice(0, separator);
const seconds = Number(head[0]);
if (!Number.isFinite(seconds) || seconds <= 0) {
throw new Error(`invalid timeout '${head[0]}'`);
}
let onTimeout: string | undefined;
for (let i = 1; i < head.length; i++) {
if (head[i] === '--on-timeout' && i + 1 < head.length) {
onTimeout = head[++i];
} else {
throw new Error(`unexpected argument '${head[i]}'`);
}
}
const [command, ...args] = argv.slice(separator + 1);
return { seconds, onTimeout, command, args };
}
function killTree(pid: number): void {
if (process.platform === 'win32') {
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
} else {
try {
// The child leads its own process group (detached), so this reaches its descendants.
process.kill(-pid, 'SIGKILL');
} catch {
// already exited
}
}
}
/** Runs a shell command, killing its process tree if it runs longer than the deadline. */
function runCleanup(command: string, deadlineMs: number): Promise<string | undefined> {
return new Promise(resolve => {
const child = spawn(command, { stdio: 'inherit', shell: true, detached: process.platform !== 'win32' });
const timer = setTimeout(() => {
if (child.pid !== undefined) {
killTree(child.pid);
}
resolve(`no exit within ${deadlineMs / 1000} s`);
}, deadlineMs);
child.on('error', err => {
clearTimeout(timer);
resolve(err.message);
});
child.on('exit', code => {
clearTimeout(timer);
resolve(code === 0 ? undefined : `exit code ${code}`);
});
});
}
export function runWithTimeout(options: Options, onTimeoutDeadlineMs = ON_TIMEOUT_DEADLINE_MS): Promise<number> {
return new Promise(resolve => {
const child = spawn(options.command, options.args, {
stdio: 'inherit',
detached: process.platform !== 'win32',
});
// The child runs in its own process group on POSIX, so a signal that cancels this
// script (for example a cancelled pipeline step) would not reach it. Stop the child's
// process tree first, then exit the way the signal would have.
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'] as const;
const onSignal = (signal: NodeJS.Signals) => {
if (child.pid !== undefined) {
killTree(child.pid);
}
process.exit(128 + (signal === 'SIGINT' ? 2 : signal === 'SIGTERM' ? 15 : 1));
};
for (const signal of signals) {
process.on(signal, onSignal);
}
const removeSignalHandlers = () => {
for (const signal of signals) {
process.off(signal, onSignal);
}
};
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
console.log(`##vso[task.logissue type=error]'${options.command}' did not finish within ${options.seconds} s and was stopped.`);
if (child.pid !== undefined) {
killTree(child.pid);
}
}, options.seconds * 1000);
child.on('error', err => {
clearTimeout(timer);
removeSignalHandlers();
console.error(err);
resolve(1);
});
child.on('exit', async (code, signal) => {
clearTimeout(timer);
removeSignalHandlers();
if (timedOut) {
if (options.onTimeout) {
const failure = await runCleanup(options.onTimeout, onTimeoutDeadlineMs);
if (failure) {
console.log(`##vso[task.logissue type=warning]'${options.onTimeout}' did not complete: ${failure}`);
}
}
resolve(TIMEOUT_EXIT_CODE);
} else {
resolve(code ?? (signal ? 1 : 0));
}
});
});
}
if (import.meta.main) {
runWithTimeout(parseArgs(process.argv.slice(2))).then(code => process.exit(code), err => {
console.error(err);
process.exit(1);
});
}
@@ -19,6 +19,8 @@ steps:
displayName: Compile test suites (non-OSS)
condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- template: ../../common/restore-playwright-browsers.yml@self
- script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install"
env:
GITHUB_TOKEN: "$(github-token-code-oss)"
@@ -19,6 +19,8 @@ steps:
displayName: Compile test suites (non-OSS)
condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- template: ../../common/restore-playwright-browsers.yml@self
- script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install"
env:
GITHUB_TOKEN: "$(github-token-code-oss)"
@@ -167,6 +167,13 @@ extends:
- repository: vscode_loc
tsa:
enabled: false
# BinSkim still scans and reports, but does not break this build. The 1ES
# templates only break on BinSkim results when a pipeline is not official
# with TSA enabled. The product build is, so it files these results in TSA
# instead, while this never-shipping build has TSA off and would fail on
# existing findings in the server's native modules.
binskim:
break: false
codeql:
compiled:
enabled: false
@@ -19,6 +19,8 @@ steps:
displayName: Compile test suites (non-OSS)
condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- template: ../../common/restore-playwright-browsers.yml@self
- powershell: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install"
env:
GITHUB_TOKEN: "$(github-token-code-oss)"
+65 -63
View File
@@ -31,33 +31,52 @@ function getEntitlementsForFile(filePath: string): string {
return path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist');
}
async function retrySignOnKeychainError<T>(fn: () => Promise<T>, maxRetries: number = 3): Promise<T> {
let lastError: Error | undefined;
function getKeychainPath(tempDir: string): string {
return path.join(tempDir, 'buildagent.keychain');
}
for (let attempt = 1; attempt <= maxRetries; attempt++) {
/**
* Usage descriptions shown by macOS when the app requests access to protected resources.
*/
const infoPlistUsageDescriptions: Record<string, string> = {
NSAppleEventsUsageDescription: 'An application in Visual Studio Code wants to use AppleScript.',
NSMicrophoneUsageDescription: 'An application in Visual Studio Code wants to use the Microphone.',
NSCameraUsageDescription: 'An application in Visual Studio Code wants to use the Camera.',
NSAudioCaptureUsageDescription: 'An application in Visual Studio Code wants to use Audio Capture.',
NSLocalNetworkUsageDescription: 'The app uses your local network for DNS resolution and to connect to locally running services.',
};
/**
* `codesign` errors caused by short-lived problems on the build agent (keychain access)
* or in Apple's timestamp service, which usually clear up within seconds.
*/
const transientSignErrors = [
'The specified item could not be found in the keychain.',
'A timestamp was expected but was not found.',
'The timestamp service is not available.',
];
/**
* Runs `fn`, retrying with exponential backoff when it fails with one of the
* {@link transientSignErrors}. Signing the app again is safe because every file
* is signed with `codesign --force`, which replaces existing signatures.
*/
export async function retryOnTransientSignError<T>(fn: () => Promise<T>, maxAttempts = 5, initialDelayMs = 10_000): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
// Check if this is the specific keychain error we want to retry
const errorMessage = error instanceof Error ? error.message : String(error);
const isKeychainError = errorMessage.includes('The specified item could not be found in the keychain.');
if (!isKeychainError || attempt === maxRetries) {
const message = error instanceof Error ? error.message : String(error);
if (attempt >= maxAttempts || !transientSignErrors.some(transientError => message.includes(transientError))) {
throw error;
}
console.log(`Signing attempt ${attempt} failed with keychain error, retrying...`);
console.log(`Error: ${errorMessage}`);
const delay = 1000 * Math.pow(2, attempt - 1);
console.log(`Waiting ${Math.round(delay)}ms before retry ${attempt}/${maxRetries}...`);
await new Promise(resolve => setTimeout(resolve, delay));
const delayMs = initialDelayMs * 2 ** (attempt - 1);
console.log(`Signing attempt ${attempt}/${maxAttempts} failed with a transient error, retrying in ${delayMs / 1000}s...`);
console.log(`Error: ${message}`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw lastError;
}
async function main(buildDir?: string): Promise<void> {
@@ -73,6 +92,10 @@ async function main(buildDir?: string): Promise<void> {
throw new Error('$AGENT_TEMPDIRECTORY not set');
}
if (!arch) {
throw new Error('$VSCODE_ARCH not set');
}
const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`);
const appName = product.nameLong + '.app';
const infoPlistPath = path.resolve(appRoot, appName, 'Contents', 'Info.plist');
@@ -86,7 +109,7 @@ async function main(buildDir?: string): Promise<void> {
}),
preAutoEntitlements: false,
preEmbedProvisioningProfile: false,
keychain: path.join(tempDir, 'buildagent.keychain'),
keychain: getKeychainPath(tempDir),
version: getElectronVersion(),
identity,
};
@@ -94,57 +117,36 @@ async function main(buildDir?: string): Promise<void> {
// Only overwrite plist entries for x64 and arm64 builds,
// universal will get its copy from the x64 build.
if (arch !== 'universal') {
await spawn('plutil', [
'-insert',
'NSAppleEventsUsageDescription',
'-string',
'An application in Visual Studio Code wants to use AppleScript.',
`${infoPlistPath}`
]);
await spawn('plutil', [
'-replace',
'NSMicrophoneUsageDescription',
'-string',
'An application in Visual Studio Code wants to use the Microphone.',
`${infoPlistPath}`
]);
await spawn('plutil', [
'-replace',
'NSCameraUsageDescription',
'-string',
'An application in Visual Studio Code wants to use the Camera.',
`${infoPlistPath}`
]);
await spawn('plutil', [
'-replace',
'NSAudioCaptureUsageDescription',
'-string',
'An application in Visual Studio Code wants to use Audio Capture.',
`${infoPlistPath}`
]);
await spawn('plutil', [
'-insert',
'NSLocalNetworkUsageDescription',
'-string',
'The app uses your local network for DNS resolution and to connect to locally running services.',
`${infoPlistPath}`
]);
// `-replace` adds missing keys and overwrites existing ones, so signing the same app again does not fail
for (const [key, value] of Object.entries(infoPlistUsageDescriptions)) {
await spawn('plutil', ['-replace', key, '-string', value, infoPlistPath]);
}
}
await retrySignOnKeychainError(() => sign(appOpts));
await retryOnTransientSignError(() => sign(appOpts));
}
async function printKeychainDiagnostics(): Promise<void> {
const tempDir = process.env['AGENT_TEMPDIRECTORY'];
if (!tempDir) {
return;
}
const keychain = getKeychainPath(tempDir);
try {
const identities = await spawn('security', ['find-identity', '-p', 'codesigning', '-v', keychain]);
console.error(`Available identities:\n${identities}`);
const dump = await spawn('security', ['dump-keychain', keychain]);
console.error(`Keychain dump:\n${dump}`);
} catch (err) {
console.error('Failed to collect keychain diagnostics:', err);
}
}
if (import.meta.main) {
main(process.argv[2]).catch(async err => {
console.error(err);
const tempDir = process.env['AGENT_TEMPDIRECTORY'];
if (tempDir) {
const keychain = path.join(tempDir, 'buildagent.keychain');
const identities = await spawn('security', ['find-identity', '-p', 'codesigning', '-v', keychain]);
console.error(`Available identities:\n${identities}`);
const dump = await spawn('security', ['dump-keychain', keychain]);
console.error(`Keychain dump:\n${dump}`);
}
await printKeychainDiagnostics();
process.exit(1);
});
}
+18
View File
@@ -523,6 +523,24 @@
"status": "enforced"
}
},
{
"key": "chat.canvases.enabled",
"name": "ChatCanvases",
"category": "InteractiveSession",
"minimumVersion": "1.144",
"localization": {
"description": {
"key": "chat.canvases.enabled.policy",
"value": "Controls whether Copilot sessions can use Canvases to present interactive content."
}
},
"type": "boolean",
"default": false,
"included": true,
"agentHost": {
"status": "enforced"
}
},
{
"key": "chat.copilotCliCommand.enabled",
"name": "CopilotCliCommand",
@@ -45,6 +45,7 @@ suite('Agent Host PR test selection', () => {
for (const filename of [
'src/vs/workbench/browser/editor.ts',
'README.md',
'build/azure-pipelines/product-build.yml',
'src/vs/platform/agentHost/node/copilot/copilotAgent.ts',
'src/vs/base/common/async.ts',
]) {
@@ -52,6 +53,7 @@ suite('Agent Host PR test selection', () => {
results.push({ affected: outputs.affected, profile: outputs.profile });
}
assert.deepStrictEqual(results, [
{ affected: 'false', profile: 'pr' },
{ affected: 'false', profile: 'pr' },
{ affected: 'false', profile: 'pr' },
{ affected: 'true', profile: 'pr' },
@@ -76,6 +78,7 @@ suite('Agent Host PR test selection', () => {
'scripts/test-integration.sh',
'scripts/test-integration.bat',
'build/agent-sdk/agents/copilot/package.json',
'build/azure-pipelines/linux/setup-env.sh',
'build/lib/test/agentHostTestSelection.test.ts',
'package.json',
'package-lock.json',
+49
View File
@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* 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 'node:assert/strict';
import { suite, test } from 'node:test';
import { retryOnTransientSignError } from '../../darwin/sign.ts';
function codesignError(stderr: string): Error {
return new Error(`Command failed: codesign --sign ABC --force --timestamp --options runtime locale.pak\nlocale.pak: ${stderr}`);
}
async function sign(failures: Error[], maxAttempts: number): Promise<{ attempts: number; result?: string; error?: string }> {
let attempts = 0;
try {
const result = await retryOnTransientSignError(async () => {
const failure = failures[attempts++];
if (failure) {
throw failure;
}
return 'signed';
}, maxAttempts, 0);
return { attempts, result };
} catch (error) {
return { attempts, error: (error as Error).message.split('\n')[1] };
}
}
suite('darwin sign', () => {
test('retries keychain and timestamp service errors until signing succeeds', async () => {
assert.deepStrictEqual(await sign([
codesignError('The specified item could not be found in the keychain.'),
codesignError('A timestamp was expected but was not found.'),
codesignError('The timestamp service is not available.'),
], 5), { attempts: 4, result: 'signed' });
});
test('does not retry other signing errors', async () => {
assert.deepStrictEqual(await sign([
codesignError('code object is not signed at all'),
], 5), { attempts: 1, error: 'locale.pak: code object is not signed at all' });
});
test('gives up after the maximum number of attempts', async () => {
const timestampError = codesignError('A timestamp was expected but was not found.');
assert.deepStrictEqual(await sign([timestampError, timestampError, timestampError], 3), { attempts: 3, error: 'locale.pak: A timestamp was expected but was not found.' });
});
});
+39 -2
View File
@@ -145,6 +145,10 @@ suite('Detect Changes', () => {
['.github/workflows/pr-linux-test.yml', ['true', 'true', 'true']],
['.github/actions/detect-changes/detect.ts', ['true', 'true', 'true']],
['build/gulpfile.ts', ['true', 'true', 'true']],
['build/azure-pipelines/product-build.yml', ['false', 'false', 'false']],
['build/azure-pipelines/win32/steps/product-build-win32-test.yml', ['false', 'false', 'false']],
['build/azure-pipelines/linux/setup-env.sh', ['true', 'true', 'true']],
['build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts', ['true', 'true', 'true']],
['package.json', ['true', 'true', 'true']],
];
const results: [string, string[]][] = [];
@@ -159,13 +163,45 @@ suite('Detect Changes', () => {
await detectCoreTests('extensions/copilot/package.json', 'README.md', 'cli/Cargo.toml'),
await detectCoreTests('extensions/copilot/package.json', 'extensions/git/package.json'),
await detectCoreTests('extensions/copilot/package.json', 'src/vs/base/common/async.ts'),
await detectCoreTests('build/azure-pipelines/product-build.yml', 'README.md'),
await detectCoreTests('build/azure-pipelines/product-build.yml', 'build/azure-pipelines/win32/exec.ps1'),
], [
['false', 'false', 'true'],
['false', 'true', 'true'],
['true', 'true', 'true'],
['false', 'false', 'false'],
['true', 'true', 'true'],
]);
});
test('a change to the Azure Pipelines definitions runs only Compile & Hygiene, whose build script tests read them', async () => {
const outputs = await detect([
{ filename: 'build/azure-pipelines/product-build.yml' },
{ filename: 'build/azure-pipelines/darwin/steps/product-build-darwin-test.yml' },
]);
const { jobs } = loadWorkflow();
assert.deepStrictEqual({
outputs,
// The jobs that the outputs do not skip. The calling jobs of the test workflows always run, while the jobs
// of the workflows skip (see the test of the "<OS> / <job>" checks below).
unskippableJobs: Object.entries(jobs)
.filter(([, job]) => !testWorkflow.test(job.uses ?? '') && !/needs\.changes\.outputs\.[\w-]+ == 'true'/.test(job.if ?? ''))
.map(([id]) => id),
}, {
outputs: {
'cli': 'false',
'copilot-shim': 'false',
'copilot': 'false',
'unit-tests': 'false',
'browser-integration': 'false',
'product-tests': 'false',
'monaco-editor': 'false',
'telemetry': 'false',
},
unskippableJobs: ['changes', 'compile', 'pr-gate'],
});
});
test('both sides of a rename participate in selection', async () => {
const outputs = await detect([{ filename: 'docs/async.md', previous_filename: 'src/vs/base/common/async.ts' }]);
assert.deepStrictEqual(coreTestOutputs.map(output => outputs[output]), ['true', 'true', 'true']);
@@ -342,6 +378,7 @@ suite('Detect Changes', () => {
test('compile, Monaco Editor and telemetry run after Detect Changes and appear first in the job group', () => {
const { jobs } = loadWorkflow();
const skipsWhenUnaffected = (output: string) => `\${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.${output} == 'true') }}`;
assert.deepStrictEqual(Object.entries(jobs).slice(0, 4).map(([id, job]) => ({
id,
name: job.name,
@@ -351,8 +388,8 @@ suite('Detect Changes', () => {
})), [
{ id: 'changes', name: 'Detect Changes', needs: undefined, if: undefined, uses: undefined },
{ id: 'compile', name: 'Compile & Hygiene', needs: 'changes', if: '${{ !cancelled() }}', uses: undefined },
{ id: 'monaco-editor', name: 'Monaco Editor', needs: 'changes', if: '${{ !cancelled() }}', uses: undefined },
{ id: 'telemetry', name: 'Telemetry', needs: 'changes', if: '${{ !cancelled() }}', uses: undefined },
{ id: 'monaco-editor', name: 'Monaco Editor', needs: 'changes', if: skipsWhenUnaffected('monaco-editor'), uses: undefined },
{ id: 'telemetry', name: 'Telemetry', needs: 'changes', if: skipsWhenUnaffected('telemetry'), uses: undefined },
]);
});
+22 -5
View File
@@ -15,17 +15,34 @@ import {
normalizeOrtLibraryName,
requiredDependencyLibraryNames,
} from '../../dictation-runtime/nuget.ts';
import { getRuntimeVersion, resolveSdkPackageRoot, SDK_PACKAGE_NAME } from '../../dictation-runtime/common.ts';
const dependencies: IFoundryDependencyVersions = {
onnxruntime: { version: '1.28.0' },
'onnxruntime-genai': { version: '0.15.2' },
onnxruntime: { version: '1.31.0' },
'onnxruntime-genai': { version: '0.18.0' },
};
suite('dictation runtime', () => {
test('matches the installed SDK and its native dependencies to the packaged runtime', () => {
const sdkRoot = resolveSdkPackageRoot();
const sdkPackage = JSON.parse(fs.readFileSync(path.join(sdkRoot, 'package.json'), 'utf8'));
const sdkDependencies = JSON.parse(fs.readFileSync(path.join(sdkRoot, 'deps_versions.json'), 'utf8'));
const rootPackage = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, '../../../package.json'), 'utf8'));
assert.deepStrictEqual({
version: sdkPackage.version,
artifacts: getStandardArtifacts(sdkDependencies),
allowedInstallScript: rootPackage.allowScripts[`${SDK_PACKAGE_NAME}@${getRuntimeVersion()}`],
}, {
version: getRuntimeVersion(),
artifacts: getStandardArtifacts(dependencies),
allowedInstallScript: true,
});
});
test('uses the Foundry Local 2.x dependency artifact set', () => {
assert.deepStrictEqual(getStandardArtifacts(dependencies), [
{ name: 'Microsoft.ML.OnnxRuntime', version: '1.28.0' },
{ name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: '0.15.2' },
{ name: 'Microsoft.ML.OnnxRuntime', version: '1.31.0' },
{ name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: '0.18.0' },
]);
});
@@ -63,7 +80,7 @@ suite('dictation runtime', () => {
}
await assert.rejects(
fetchDependencyLibraries(target, getStandardArtifacts(dependencies), targetDirectory, { feeds: [] }),
/Failed to download Microsoft\.ML\.OnnxRuntime 1\.28\.0 from any feed/,
/Failed to download Microsoft\.ML\.OnnxRuntime 1\.31\.0 from any feed/,
);
}
} finally {
+91
View File
@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* 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 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { suite, test } from 'node:test';
import { fileURLToPath } from 'node:url';
import { parseArgs, TIMEOUT_EXIT_CODE } from '../../azure-pipelines/common/runWithTimeout.ts';
const scriptPath = fileURLToPath(new URL('../../azure-pipelines/common/runWithTimeout.ts', import.meta.url));
function run(args: string[], env: NodeJS.ProcessEnv = {}) {
return spawnSync(process.execPath, [scriptPath, ...args], { encoding: 'utf8', timeout: 30_000, env: { ...process.env, ...env } });
}
suite('runWithTimeout', () => {
test('passes through the exit code of a command that finishes in time', () => {
assert.equal(run(['10', '--', process.execPath, '-e', 'process.exit(0)']).status, 0);
assert.equal(run(['10', '--', process.execPath, '-e', 'process.exit(3)']).status, 3);
});
test('fails a command that runs past the deadline and runs the timeout command', () => {
const dir = mkdtempSync(join(tmpdir(), 'run-with-timeout-'));
const marker = join(dir, 'on-timeout');
try {
const started = Date.now();
const result = run(['1', '--on-timeout', `"${process.execPath}" -e "require('fs').writeFileSync(process.argv[1], '')" "${marker}"`, '--', process.execPath, '-e', 'setTimeout(() => {}, 60_000)']);
assert.equal(result.status, TIMEOUT_EXIT_CODE);
assert.ok(Date.now() - started < 20_000, 'the hung command was not stopped');
assert.match(result.stdout, /##vso\[task\.logissue type=error\].*did not finish within 1 s/);
assert.ok(existsSync(marker), 'the timeout command did not run');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('still fails with the timeout exit code when the timeout command fails', () => {
const result = run(['1', '--on-timeout', `"${process.execPath}" -e "process.exit(5)"`, '--', process.execPath, '-e', 'setTimeout(() => {}, 60_000)']);
assert.equal(result.status, TIMEOUT_EXIT_CODE);
assert.match(result.stdout, /did not complete: exit code 5/);
});
test('kills a hung timeout command and still fails with the timeout exit code', () => {
const dir = mkdtempSync(join(tmpdir(), 'run-with-timeout-'));
const marker = join(dir, 'cleanup-survived');
try {
// The timeout command would write the marker after 4 s, unless it's killed at its 1 s deadline.
const cleanup = `"${process.execPath}" -e "setTimeout(() => require('fs').writeFileSync(process.argv[1], ''), 4000)" "${marker}"`;
const result = run(['1', '--on-timeout', cleanup, '--', process.execPath, '-e', 'setTimeout(() => {}, 60_000)'], { RUN_WITH_TIMEOUT_CLEANUP_MS: '1000' });
assert.equal(result.status, TIMEOUT_EXIT_CODE);
assert.match(result.stdout, /did not complete: no exit within 1 s/);
spawnSync(process.execPath, ['-e', 'setTimeout(() => {}, 5000)']);
assert.equal(existsSync(marker), false, 'the timeout command kept running after its deadline');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('stops the command when the script itself is cancelled', { skip: process.platform === 'win32' }, async () => {
const dir = mkdtempSync(join(tmpdir(), 'run-with-timeout-'));
const pidFile = join(dir, 'pid');
try {
const wrapper = spawn(process.execPath, [scriptPath, '600', '--', process.execPath, '-e', `require('fs').writeFileSync(process.argv[1], String(process.pid)); setTimeout(() => {}, 60_000)`, pidFile], { stdio: 'ignore' });
const deadline = Date.now() + 10_000;
while (!existsSync(pidFile) || !readFileSync(pidFile, 'utf8')) {
assert.ok(Date.now() < deadline, 'the command did not start');
await new Promise(resolve => setTimeout(resolve, 50));
}
const childPid = Number(readFileSync(pidFile, 'utf8'));
const exited = new Promise<number | null>(resolve => wrapper.on('exit', code => resolve(code)));
wrapper.kill('SIGTERM');
assert.equal(await exited, 143);
await new Promise(resolve => setTimeout(resolve, 500));
assert.throws(() => process.kill(childPid, 0), /ESRCH/, 'the command kept running after the script was cancelled');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('rejects malformed arguments', () => {
assert.throws(() => parseArgs(['10', 'cargo']));
assert.throws(() => parseArgs(['0', '--', 'cargo']));
assert.throws(() => parseArgs(['10', '--bogus', '--', 'cargo']));
assert.deepEqual(parseArgs(['600', '--on-timeout', 'sccache --stop-server', '--', 'cargo', 'clippy']), { seconds: 600, onTimeout: 'sccache --stop-server', command: 'cargo', args: ['clippy'] });
});
});
+2 -2
View File
@@ -1727,8 +1727,8 @@ begin
Command :=
'-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command "' +
'Get-CimInstance Win32_Process -Filter ""Name = ''dllhost.exe''"" | ' +
'Where-Object { $_.CommandLine -like ''*/Processid:{#FileExplorerContextMenuCLSID}*'' } | ' +
'Get-CimInstance Win32_Process -Filter \"Name = ''dllhost.exe''\" | ' +
'Where-Object { $_.CommandLine -like ''*{#FileExplorerContextMenuCLSID}*'' } | ' +
'ForEach-Object { try { Stop-Process -Id $_.ProcessId -Force -ErrorAction Stop } catch {} }"';
if not ShellExec('', 'powershell.exe', Command, '', SW_HIDE, ewWaitUntilTerminated, KillErrorCode) then
-23
View File
@@ -160,29 +160,6 @@
"group": "1_launch"
}
},
{
"name": "Run tool call s-test",
"internalConsoleOptions": "openOnSessionStart",
"runtimeExecutable": "npm",
"args": [
"run",
"simulate",
"--",
"--verbose",
"-n=1",
"-p=1",
"--sidebar",
"--scenario-test=toolcall.stest",
"--external-scenarios=${workspaceFolder}/test/toolcalls",
"--output=${workspaceFolder}/test/out/"
],
"request": "launch",
"type": "node",
"presentation": {
"group": "1_launch"
}
},
{
"type": "node",
"name": "Postinstall",
+2
View File
@@ -99,6 +99,8 @@ npm run test:extension
Finally, there are **simulation tests**. These tests reach out to Copilot API endpoints, invoke LLMs and require expensive computations to run. Each test runs 10 times, to accommodate for the stochastic nature of LLMs themselves. The results of all runs of all tests are snapshotted in the baseline file, [`test/simulation/baseline.json`](test/simulation/baseline.json), which encodes the quality of the test suite at any given point in time.
The legacy chat-panel and code-editor inline chat-widget simulation suites have been retired, including the multi-file Edit-mode variants and all suites labelled `[panel]`. Panel chat now runs through the Agent Host Protocol; add panel end-to-end coverage to the [Agent Host E2E tests](../../src/vs/platform/agentHost/test/node/e2e/README.md). Non-panel code-mapper, notebook, standalone review, and prompt tests remain, as do inline-completion and next-edit-suggestion simulations. This test cleanup does not remove runtime features or change CI gates.
Because LLM results are both random and costly, they are cached within the repo in `test/simulation/cache`. This means rerunning the simulation tests and benefiting from the cache will make the test run be both faster as well as deterministic.
You can run the simulation tests with:
@@ -1,11 +0,0 @@
---
name: project-setup-info-context7
description: "Comprehensive setup steps to help the user create complete project structures in a VS Code workspace. This tool is designed for full project initialization and scaffolding, not for creating individual files. When to use this tool: when the user wants to create a new complete project from scratch; when setting up entire project frameworks (TypeScript projects, React apps, Node.js servers, etc.); when initializing Model Context Protocol (MCP) servers with full structure; when creating VS Code extensions with proper scaffolding; when setting up Next.js, Vite, or other framework-based projects; when the user asks for \"new project\", \"create a workspace\", or \"set up a [framework] project\"; when you need to establish a complete development environment with dependencies, config files, and folder structure. When NOT to use this tool: when creating single files or small code snippets; when adding individual files to existing projects; when making modifications to existing codebases; when the user asks to \"create a file\" or \"add a component\"; for simple code examples or demonstrations; for debugging or fixing existing code. This tool provides complete project setup including folder structure creation, package.json and dependency management, configuration files (tsconfig, eslint, etc.), initial boilerplate code, development environment setup, and build and run instructions. Use other file creation tools for individual files within existing projects."
---
# How to setup a project using context7 tools
Use context7 tools to find the latest libraries, APIs, and documentation to help the user create and customize their project. Only setup a project if the folder is empty or if you've just called the tool first calling the tool to create a workspace.
1. Call the `mcp_context7_resolve-library-id` tool with your project requirements.
2. Call the `mcp_context7_get-library-docs` tool to get scaffolding instructions.
@@ -1,114 +0,0 @@
---
name: project-setup-info-local
description: 'Comprehensive setup steps to help the user create complete project structures in a VS Code workspace; this tool is designed for full project initialization and scaffolding, not for creating individual files. When to use this tool: user wants to create a new complete project from scratch; setting up entire project frameworks (TypeScript projects, React apps, Node.js servers, etc.); initializing Model Context Protocol (MCP) servers with full structure; creating VS Code extensions with proper scaffolding; setting up Next.js, Vite, or other framework-based projects; user asks for "new project", "create a workspace", "set up a [framework] project"; need to establish a complete development environment with dependencies, config files, and folder structure. When NOT to use this tool: creating single files or small code snippets; adding individual files to existing projects; making modifications to existing codebases; user asks to "create a file" or "add a component"; simple code examples or demonstrations; debugging or fixing existing code. This tool provides complete project setup including: folder structure creation; package.json and dependency management; configuration files (tsconfig, eslint, etc.); initial boilerplate code; development environment setup; build and run instructions. Use other file creation tools for individual files within existing projects.'
---
# How to setup a project
Determine what kind of project the user wants to create, then based on that, choose which setup info below to follow. Only setup a project if the folder is empty or if you've just called the tool first calling the tool to create a workspace.
## vscode-extension
A template for creating a VS Code extension using Yeoman and Generator-Code.
Run this command:
```
npx --package yo --package generator-code -- yo code . --skipOpen
```
The command has the following arguments:
- `-t, --extensionType`: Specify extension type: ts, js, command-ts, command-js, colortheme, language, snippets, keymap, extensionpack, localization, commandweb, notebook. Defaults to `ts`
- `-n, --extensionDisplayName`: Set the display name of the extension.
- `--extensionId`: Set the unique ID of the extension. Do not select this option if the user has not requested a unique ID.
- `--extensionDescription`: Provide a description for the extension.
- `--pkgManager`: Specify package manager: npm, yarn, or pnpm. Defaults to `npm`.
- `--bundler`: Bundle the extension using webpack or esbuild.
- `--gitInit`: Initialize a Git repository for the extension.
- `--snippetFolder`: Specify the location of the snippet folder.
- `--snippetLanguage`: Set the language for snippets.
### Rules
1. Do not remove any arguments from the command. Only add arguments if the user requests them.
2. Call the tool `get_vscode_api` with the user's query to get the relevant references.
3. After the tool `get_vscode_api` has completed, only then begin to modify the project.
## next-js
A React based framework for building server-rendered web applications.
Run this command:
```
npx create-next-app@latest .
```
The command has the following arguments:
- `--ts, --typescript`: Initialize as a TypeScript project. This is the default.
- `--js, --javascript`: Initialize as a JavaScript project.
- `--tailwind`: Initialize with Tailwind CSS config. This is the default.
- `--eslint`: Initialize with ESLint config.
- `--app`: Initialize as an App Router project.
- `--src-dir`: Initialize inside a 'src/' directory.
- `--turbopack`: Enable Turbopack by default for development.
- `--import-alias <prefix/*>`: Specify import alias to use.(default is "@/*")
- `--api`: Initialize a headless API using the App Router.
- `--empty`: Initialize an empty project.
- `--use-npm`: Explicitly tell the CLI to bootstrap the application using npm.
- `--use-pnpm`: Explicitly tell the CLI to bootstrap the application using pnpm.
- `--use-yarn`: Explicitly tell the CLI to bootstrap the application using Yarn.
- `--use-bun`: Explicitly tell the CLI to bootstrap the application using Bun.
## vite
A front end build tool for web applications that focuses on speed and performance. Can be used with React, Vue, Preact, Lit, Svelte, Solid, and Qwik.
Run this command:
```
npx create-vite@latest .
```
The command has the following arguments:
- `-t, --template NAME`: Use a specific template. Available templates: vanilla-ts, vanilla, vue-ts, vue, react-ts, react, react-swc-ts, react-swc, preact-ts, preact, lit-ts, lit, svelte-ts, svelte, solid-ts, solid, qwik-ts, qwik
## mcp-server
A Model Context Protocol (MCP) server project. This project supports multiple programming languages including TypeScript, JavaScript, Python, C#, Java, and Kotlin.
### Rules
1. First, visit https://github.com/modelcontextprotocol to find the correct SDK and setup instructions for the requested language. Default to TypeScript if no language is specified.
2. Use the `fetch_webpage` tool to find the correct implementation instructions from https://modelcontextprotocol.io/llms-full.txt
3. Update the copilot-instructions.md file in the .github directory to include references to the SDK documentation
4. Create an `mcp.json` file in the `.vscode` folder in the project root with the following content: `{ "servers": { "mcp-server-name": { "type": "stdio", "command": "command-to-run", "args": [list-of-args] } } }`.
- mcp-server-name: The name of the MCP server. Create a unique name that reflects what this MCP server does.
- command-to-run: The command to run to start the MCP server. This is the command you would use to run the project you just created.
- list-of-args: The arguments to pass to the command. This is the list of arguments you would use to run the project you just created.
5. Recommend any required VS Code extensions based on the chosen language (e.g., Python extension for Python projects) by adding their IDs to the "recommendations" array in `.vscode/extensions.json`. Do not install extensions yourself.
6. Inform the user that they can now debug this MCP server using VS Code.
## python-script
A simple Python script project which should be chosen when just a single script wants to be created.
Required extensions: `ms-python.python`, `ms-python.vscode-python-envs`
### Rules
1. Create a single Python script (for example, `main.py`) in the project folder with a `main()` entry point guarded by `if __name__ == "__main__":`.
2. Add a `.gitignore` for Python and, if the project has dependencies, a `requirements.txt`.
## python-package
A Python package project which can be used to create a distributable package.
Required extensions: `ms-python.python`, `ms-python.vscode-python-envs`
### Rules
1. Create a `pyproject.toml` that declares the package name, version, and build backend.
2. Create the package in `src/<package_name>/__init__.py` and a `tests/` folder with an initial test.
3. Add a `.gitignore` for Python and a `README.md` that explains how to install the package in editable mode (`pip install -e .`).
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "copilot-chat",
"version": "0.72.0",
"version": "0.73.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "copilot-chat",
"version": "0.72.0",
"version": "0.73.0",
"hasInstallScript": true,
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
@@ -143,7 +143,7 @@
"engines": {
"node": ">=22.14.0",
"npm": ">=9.0.0",
"vscode": "^1.144.0"
"vscode": "^1.145.0"
}
},
"node_modules/@anthropic-ai/sdk": {
+2 -97
View File
@@ -2,7 +2,7 @@
"name": "copilot-chat",
"displayName": "GitHub Copilot",
"description": "AI chat features powered by Copilot",
"version": "0.72.0",
"version": "0.73.0",
"build": "1",
"completionsCoreVersion": "1.378.1799",
"internalLargeStorageAriaKey": "ec712b3202c5462fb6877acae7f1f9d7-c19ad55e-3e3c-4f99-984b-827f6d95bd9e-6917",
@@ -22,7 +22,7 @@
"icon": "assets/copilot.png",
"pricing": "Trial",
"engines": {
"vscode": "^1.144.0",
"vscode": "^1.145.0",
"npm": ">=9.0.0",
"node": ">=22.14.0"
},
@@ -575,31 +575,6 @@
}
}
},
{
"name": "copilot_createNewWorkspace",
"displayName": "%github.copilot.tools.createNewWorkspace.name%",
"toolReferenceName": "newWorkspace",
"legacyToolReferenceFullNames": [
"new/newWorkspace"
],
"icon": "$(new-folder)",
"userDescription": "%github.copilot.tools.createNewWorkspace.userDescription%",
"when": "config.github.copilot.chat.newWorkspaceCreation.enabled",
"modelDescription": "Get comprehensive setup steps to help the user create complete project structures in a VS Code workspace. This tool is designed for full project initialization and scaffolding, not for creating individual files.\n\nWhen to use this tool:\n- User wants to create a new complete project from scratch\n- Setting up entire project frameworks (TypeScript projects, React apps, Node.js servers, etc.)\n- Initializing Model Context Protocol (MCP) servers with full structure\n- Creating VS Code extensions with proper scaffolding\n- Setting up Next.js, Vite, or other framework-based projects\n- User asks for \"new project\", \"create a workspace\", \"set up a [framework] project\"\n- Need to establish complete development environment with dependencies, config files, and folder structure\n\nWhen NOT to use this tool:\n- Creating single files or small code snippets\n- Adding individual files to existing projects\n- Making modifications to existing codebases\n- User asks to \"create a file\" or \"add a component\"\n- Simple code examples or demonstrations\n- Debugging or fixing existing code\n\nThis tool provides complete project setup including:\n- Folder structure creation\n- Package.json and dependency management\n- Configuration files (tsconfig, eslint, etc.)\n- Initial boilerplate code\n- Development environment setup\n- Build and run instructions\n\nUse other file creation tools for individual files within existing projects.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to use to generate the new workspace. This should be a clear and concise description of the workspace the user wants to create."
}
},
"required": [
"query"
]
},
"tags": []
},
{
"name": "copilot_createNewJupyterNotebook",
"displayName": "Create New Jupyter Notebook",
@@ -1273,7 +1248,6 @@
"description": "",
"tools": [
"memory",
"newWorkspace",
"resolveMemoryFileUri",
"switchAgent",
"toolSearch",
@@ -1355,23 +1329,6 @@
"description": "%copilot.workspace.fix.description%",
"sampleRequest": "%copilot.workspace.fix.sampleRequest%"
},
{
"name": "new",
"description": "%copilot.workspace.new.description%",
"sampleRequest": "%copilot.workspace.new.sampleRequest%",
"isSticky": true,
"disambiguation": [
{
"category": "create_new_workspace_or_extension",
"description": "The user wants to create a complete Visual Studio Code workspace from scratch, such as a new application or a Visual Studio Code extension. Use this category only if the question relates to generating or creating new workspaces in Visual Studio Code. Do not use this category for updating existing code or generating sample code snippets",
"examples": [
"Scaffold a Node server.",
"Create a sample project which uses the fileSystemProvider API.",
"react application"
]
}
]
},
{
"name": "newNotebook",
"description": "%copilot.workspace.newNotebook.description%",
@@ -1485,23 +1442,6 @@
"description": "%copilot.workspace.fix.description%",
"sampleRequest": "%copilot.workspace.fix.sampleRequest%"
},
{
"name": "new",
"description": "%copilot.workspace.new.description%",
"sampleRequest": "%copilot.workspace.new.sampleRequest%",
"isSticky": true,
"disambiguation": [
{
"category": "create_new_workspace_or_extension",
"description": "The user wants to create a complete Visual Studio Code workspace from scratch, such as a new application or a Visual Studio Code extension. Use this category only if the question relates to generating or creating new workspaces in Visual Studio Code. Do not use this category for updating existing code or generating sample code snippets",
"examples": [
"Scaffold a Node server.",
"Create a sample project which uses the fileSystemProvider API.",
"react application"
]
}
]
},
{
"name": "newNotebook",
"description": "%copilot.workspace.newNotebook.description%",
@@ -3237,11 +3177,6 @@
"default": false,
"markdownDescription": "%github.copilot.config.scopeSelection%"
},
"github.copilot.chat.useProjectTemplates": {
"type": "boolean",
"default": true,
"markdownDescription": "%github.copilot.config.useProjectTemplates%"
},
"github.copilot.nextEditSuggestions.enabled": {
"type": "boolean",
"default": true,
@@ -3901,22 +3836,6 @@
],
"markdownDescription": "%github.copilot.chat.languageContext.inline.typescript.enabled%"
},
"github.copilot.chat.newWorkspaceCreation.enabled": {
"type": "boolean",
"default": true,
"tags": [
"experimental"
],
"description": "%github.copilot.config.newWorkspaceCreation.enabled%"
},
"github.copilot.chat.newWorkspace.useContext7": {
"type": "boolean",
"default": false,
"tags": [
"experimental"
],
"markdownDescription": "%github.copilot.config.newWorkspace.useContext7%"
},
"github.copilot.chat.notebook.followCellExecution.enabled": {
"type": "boolean",
"default": false,
@@ -6755,20 +6674,6 @@
}
],
"chatSkills": [
{
"path": "./assets/prompts/skills/project-setup-info-local/SKILL.md",
"when": "!config.github.copilot.chat.newWorkspace.useContext7",
"sessionTypes": [
"local"
]
},
{
"path": "./assets/prompts/skills/project-setup-info-context7/SKILL.md",
"when": "config.github.copilot.chat.newWorkspace.useContext7",
"sessionTypes": [
"local"
]
},
{
"path": "./assets/prompts/skills/troubleshoot/SKILL.md",
"sessionTypes": [
-7
View File
@@ -188,8 +188,6 @@
"copilot.workspace.tests.description": "Generate unit tests for the selected code",
"copilot.workspace.fix.description": "Propose a fix for the problems in the selected code",
"copilot.workspace.fix.sampleRequest": "There is a problem in this code. Rewrite the code to show it with the bug fixed.",
"copilot.workspace.new.description": "Scaffold code for a new file or project in a workspace",
"copilot.workspace.new.sampleRequest": "Create a RESTful API server using typescript",
"copilot.workspace.newNotebook.description": "Create a new Jupyter Notebook",
"copilot.workspace.newNotebook.sampleRequest": "How do I create a notebook to load data from a csv file?",
"copilot.workspace.semanticSearch.description": "Find relevant code to your query",
@@ -214,7 +212,6 @@
"github.copilot.submenu.reviewComment.applyAndNext.label": "Apply and Go to Next",
"github.copilot.submenu.reviewComment.discardAndNext.label": "Discard and Go to Next",
"github.copilot.submenu.reviewComment.discard.label": "Discard",
"github.copilot.config.useProjectTemplates": "Use relevant GitHub projects as starter projects when using `/new`",
"github.copilot.chat.attachFile": "Add File to Chat",
"github.copilot.chat.attachSelection": "Add Selection to Chat",
"github.copilot.command.collectDiagnostics": "Chat Diagnostics",
@@ -266,12 +263,9 @@
"github.copilot.tools.createAndRunTask.name": "Create and Run Task",
"github.copilot.tools.createAndRunTask.description": "Create and run a task in the workspace",
"github.copilot.tools.createAndRunTask.userDescription": "Create and run a task in the workspace",
"github.copilot.config.newWorkspaceCreation.enabled": "Whether to enable new agentic workspace creation.",
"github.copilot.config.newWorkspace.useContext7": "Whether to use the [Context7](command:github.copilot.mcp.viewContext7) tools to scaffold project for new workspace creation.",
"github.copilot.config.editsNewNotebook.enabled": "Whether to enable the new notebook tool in Copilot Edits.",
"github.copilot.config.notebook.inlineEditAgent.enabled": "Enable agent-like behavior from the notebook inline chat widget.",
"github.copilot.config.summarizeAgentConversationHistory.enabled": "Whether to auto-compact agent conversation history once the context window is filled.",
"github.copilot.tools.createNewWorkspace.name": "Create New Workspace",
"github.copilot.tools.openEmptyFolder.name": "Open an empty folder as VS Code workspace",
"github.copilot.tools.githubRepo.name": "Semantic Search GitHub Repository",
"github.copilot.tools.githubRepo.userDescription": "Semantic Search a GitHub repository for relevant source code snippets. You can specify a repository using `owner/repo`",
@@ -283,7 +277,6 @@
"github.copilot.config.chatCompletionsTokenParameter.maxCompletionTokens": "Send `max_completion_tokens`.",
"github.copilot.config.chatCompletionsTokenParameter.maxTokens": "Send the legacy `max_tokens` parameter for compatibility.",
"github.copilot.config.inlineEdits.xtabProvider.modelConfiguration": "Advanced model configuration for the next edit suggestions xtab provider. Optional `reasoningEffort` sends `reasoning_effort` and accepts `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; supported levels depend on the model. Optional `maxCompletionTokens` is a positive integer budget for completion tokens, including reasoning tokens, and sends `max_completion_tokens` instead of `max_tokens`. When `github.copilot.chat.advanced.inlineEdits.xtabProvider.usePrediction` is true, both `useMaxTokens` and `maxCompletionTokens` are ignored and neither token limit is sent. Otherwise, setting `github.copilot.chat.advanced.inlineEdits.xtabProvider.useMaxTokens` to false suppresses both token limits. Omitting these fields preserves existing request behavior. For example: `{ \"modelName\": \"your-model\", \"includeTagsInCurrentFile\": false, \"reasoningEffort\": \"low\", \"maxCompletionTokens\": 8192 }`. The same fields are supported in the experiment-driven `modelConfigurationString` and `defaultModelConfigurationString` JSON strings.\n\n**Note**: This is an advanced setting.",
"github.copilot.tools.createNewWorkspace.userDescription": "Scaffold a new workspace in VS Code",
"github.copilot.chat.tools.grepSearch.outputFormat": "The output format for the grep search tool. Can be either 'grep' or 'tag'. The default is 'grep'.",
"github.copilot.chat.tools.grepSearch.defaultMaxResults": "The default maximum number of results to return from the grep search tool. The default is 100.",
"github.copilot.chat.tools.grepSearch.maxResultsCap": "The maximum number of results that can be returned from the grep search tool. The default is 200.",
@@ -10,7 +10,6 @@ export const enum Intent {
Review = 'review',
Tests = 'tests',
Fix = 'fix',
New = 'new',
NewNotebook = 'newNotebook',
notebookEditor = 'notebookEditor',
InlineChat = 'inlineChat',
@@ -41,7 +40,6 @@ export const agentsToCommands: Partial<Record<Intent, Record<string, Intent>>> =
'review': Intent.Review,
'tests': Intent.Tests,
'fix': Intent.Fix,
'new': Intent.New,
'newNotebook': Intent.NewNotebook,
'semanticSearch': Intent.SemanticSearch,
'setupTests': Intent.SetupTests,
@@ -55,8 +55,6 @@ export class GitHubCopilotLogger extends Disposable implements GitHubLogger {
) {
super();
this._register(onCopilotToken(authenticationService, t => this.checkCopilotToken(t)));
this.createChannel();
}
private checkCopilotToken = (token: Omit<CopilotToken, 'token'>) => {
@@ -88,10 +86,14 @@ export class GitHubCopilotLogger extends Disposable implements GitHubLogger {
return this.output.value;
}
this.output.value = new CodeReferenceOutputChannel(window.createOutputChannel(citationsChannelName, 'code-referencing'));
this.output.value = new CodeReferenceOutputChannel(this.createOutputChannel());
return this.output.value;
}
protected createOutputChannel(): OutputChannel {
return window.createOutputChannel(citationsChannelName, 'code-referencing');
}
private getChannel(): CodeReferenceOutputChannel | undefined {
return this.output.value;
}
@@ -4,13 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as Sinon from 'sinon';
import { Disposable, ExtensionContext } from 'vscode';
import { Disposable, ExtensionContext, OutputChannel } from 'vscode';
import { CodeReference } from '..';
import { IAuthenticationService } from '../../../../../../../platform/authentication/common/authentication';
import { CopilotToken, createTestExtendedTokenInfo } from '../../../../../../../platform/authentication/common/copilotToken';
import { generateUuid } from '../../../../../../../util/vs/base/common/uuid';
import { IInstantiationService } from '../../../../../../../util/vs/platform/instantiation/common/instantiation';
import { ConnectionState } from '../../../../lib/src/snippy/connectionState';
import { createExtensionTestingContext } from '../../test/context';
import { GitHubCopilotLogger } from '../outputChannel';
function testExtensionContext() {
return {
@@ -67,4 +69,30 @@ suite('CodeReference', function () {
assert.notStrictEqual(codeQuote.subscriptions, undefined);
});
});
test('output channel is created lazily', function () {
const accessor = createExtensionTestingContext().createTestingAccessor();
const authenticationService = accessor.get(IAuthenticationService);
let channelCount = 0;
const logger = new class extends GitHubCopilotLogger {
protected override createOutputChannel(): OutputChannel {
channelCount++;
return {
name: 'test',
append() { },
appendLine() { },
clear() { },
replace() { },
show() { },
hide() { },
dispose() { },
};
}
}(accessor.get(IInstantiationService), authenticationService);
assert.strictEqual(channelCount, 0);
logger.info('citation');
assert.strictEqual(channelCount, 1);
logger.dispose();
});
});
@@ -28,7 +28,6 @@ import { ContributionCollection, IExtensionContribution } from '../../common/con
import { vscodeNodeChatContributions } from '../../extension/vscode-node/contributions';
import { IMergeConflictService } from '../../git/common/mergeConflictService';
import { registerInlineChatCommands } from '../../inlineChat/vscode-node/inlineChatCommands';
import { INewWorkspacePreviewContentManager } from '../../intents/node/newIntent';
import { FindInFilesArgs } from '../../intents/node/searchIntent';
import { TerminalExplainIntent } from '../../intents/node/terminalExplainIntent';
import { ILinkifyService } from '../../linkify/common/linkifyService';
@@ -40,7 +39,6 @@ import { IntentDetector } from '../../prompt/node/intentDetector';
import { SemanticSearchTextSearchProvider } from '../../workspaceSemanticSearch/node/semanticSearchTextSearchProvider';
import { GitHubPullRequestProviders } from '../node/githubPullRequestProviders';
import { startFeedbackCollection } from './feedbackCollection';
import { registerNewWorkspaceIntentCommand } from './newWorkspaceFollowup';
import { generateTerminalFixes, setLastCommandMatchResult } from './terminalFixGenerator';
/**
@@ -79,7 +77,6 @@ export class ConversationFeature implements IExtensionContribution {
@IMergeConflictService private readonly mergeConflictService: IMergeConflictService,
@ILinkifyService private readonly linkifyService: ILinkifyService,
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
@INewWorkspacePreviewContentManager private readonly newWorkspacePreviewContentManager: INewWorkspacePreviewContentManager,
@ISettingsEditorSearchService private readonly settingsEditorSearchService: ISettingsEditorSearchService,
) {
this._enabled = false;
@@ -347,7 +344,6 @@ export class ConversationFeature implements IExtensionContribution {
}),
this.instantiationService.invokeFunction(registerInlineChatCommands),
this.registerTerminalQuickFixProviders(),
registerNewWorkspaceIntentCommand(this.newWorkspacePreviewContentManager, this.logService, options),
registerGitHubPullRequestTitleAndDescriptionProvider(this.instantiationService),
registerSearchIntentCommand(),
].forEach(d => disposables.add(d));
@@ -1,262 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as l10n from '@vscode/l10n';
import { ChatResponseFileTreePart, Disposable, MarkdownString, ProgressLocation, SaveDialogOptions, Tab, TabInputText, Uri, commands, env, interactive, window, workspace } from 'vscode';
import { IConversationOptions } from '../../../platform/chat/common/conversationOptions';
import { ILogService } from '../../../platform/log/common/logService';
import { getSafeNotificationMessage } from '../../../platform/notification/common/notificationMessage';
import * as path from '../../../util/vs/base/common/path';
import { CopilotFileScheme, CopilotWorkspaceScheme, CreateFileCommand, CreateProjectCommand, GithubWorkspaceScheme, INewWorkspacePreviewContentManager, OpenFileCommand } from '../../intents/node/newIntent';
import { NewWorkspacePreviewFileSystemProvider } from '../../intents/vscode-node/newWorkspacePreviewFileSystemProvider';
import { NewWorkspaceTextDocumentProvider } from '../../intents/vscode-node/newWorkspaceTextDocumentProvider';
import { listFilesInResponseFileTree } from '../../prompt/common/fileTreeParser';
export function registerNewWorkspaceIntentCommand(previewContentManager: INewWorkspacePreviewContentManager, logService: ILogService, options: IConversationOptions) {
const copilotWorkspaceProvider = new NewWorkspacePreviewFileSystemProvider(previewContentManager);
const githubWorkspaceProvider = new NewWorkspacePreviewFileSystemProvider(previewContentManager);
const copilotTextDocumentProvider = new NewWorkspaceTextDocumentProvider(previewContentManager);
return Disposable.from(
workspace.registerFileSystemProvider(CopilotWorkspaceScheme,
copilotWorkspaceProvider,
{ isReadonly: new MarkdownString(l10n.t('This file preview was generated by Copilot and may contain surprises or mistakes.\n\nAsk followup questions to refine it, then press Create Workspace.')) }),
workspace.registerFileSystemProvider(GithubWorkspaceScheme,
githubWorkspaceProvider,
{ isReadonly: true }),
workspace.registerTextDocumentContentProvider(CopilotFileScheme, copilotTextDocumentProvider),
commands.registerCommand(CreateProjectCommand, async (fileTreePart: ChatResponseFileTreePart, workspaceRoot: Uri | undefined) => {
const parentFolder = (await window.showOpenDialog({ defaultUri: workspaceRoot, title: path.basename(fileTreePart.baseUri.path), canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select as Parent Folder' }))?.[0];
if (!parentFolder) {
return;
}
await createWorkspace(logService, workspaceRoot, parentFolder, fileTreePart);
}),
commands.registerCommand(OpenFileCommand, async (fileTreePart: ChatResponseFileTreePart) => {
const pathStr = Uri.joinPath(fileTreePart.baseUri, fileTreePart.value[0].name).toString();
const document = await workspace.openTextDocument(Uri.parse(pathStr));
await window.showTextDocument(document, { preview: false });
}),
commands.registerCommand(CreateFileCommand, async (fileTreePart: ChatResponseFileTreePart) => {
const options: SaveDialogOptions = {
defaultUri: Uri.file(path.posix.join(workspace.workspaceFolders?.[0].uri.path ?? '', fileTreePart.value[0].name)),
saveLabel: l10n.t('Save File'),
};
const uri = await window.showSaveDialog(options);
if (uri) {
const pathStr = Uri.joinPath(fileTreePart.baseUri, fileTreePart.value[0].name).toString();
const document = await workspace.openTextDocument(Uri.parse(pathStr));
await workspace.fs.writeFile(uri, Buffer.from(document.getText()));
// Close out all previews since they won't properly restore
const tabsToClose: Tab[] = [];
window.tabGroups.all.forEach(group => {
group.tabs.forEach((tab) => {
if (tab.input instanceof TabInputText && tab.input.uri.scheme === CopilotFileScheme) {
tabsToClose.push(tab);
}
});
});
window.tabGroups.close(tabsToClose, true);
// re-open saved file
const fileDoc = await workspace.openTextDocument(Uri.file(uri.fsPath));
await window.showTextDocument(fileDoc);
}
}),
copilotWorkspaceProvider,
githubWorkspaceProvider,
copilotTextDocumentProvider,
);
}
async function createWorkspace(logService: ILogService, workspaceRoot: Uri | undefined, parentFolder: Uri, fileTreePart: ChatResponseFileTreePart) {
// Close out all previews since they won't properly restore
const tabsToClose: Tab[] = [];
window.tabGroups.all.forEach(group => {
group.tabs.forEach((tab) => {
if (tab.input instanceof TabInputText && tab.input.uri.scheme === CopilotWorkspaceScheme) {
tabsToClose.push(tab);
}
});
});
window.tabGroups.close(tabsToClose, true);
// remove path separator from the beginning of the path
const projectRoot = fileTreePart.baseUri.path.slice(1);
const projectName = await getUniqueProjectName(parentFolder, projectRoot);
const workspaceUri = Uri.joinPath(parentFolder, projectName);
const files = listFilesInResponseFileTree(fileTreePart.value);
if (files.length === 0) {
return;
}
try {
await window.withProgress({ location: ProgressLocation.Notification, cancellable: true }, async (progress, token) => {
for (const file of files) {
const fileUri = resolveProjectFileUri(workspaceUri, projectRoot, file);
// Guard against path traversal: a malicious or prompt-injected file tree
// can contain `..` segments that escape the generated workspace folder.
// Skip any file whose resolved destination is not contained within it.
if (!isUriContained(workspaceUri, fileUri)) {
logService.warn(`[newIntent] Skipping file outside of workspace: ${file}`);
continue;
}
progress.report({ message: getFileCreationProgressMessage(fileUri) });
const content = await workspace.fs.readFile(Uri.joinPath(fileTreePart.baseUri, file));
await workspace.fs.createDirectory(Uri.joinPath(fileUri, '..'));
await workspace.fs.writeFile(fileUri, content);
}
await updateAiGeneratedWorkspacesFile(workspaceUri);
});
if (workspaceRoot && workspaceUri.fsPath.startsWith(workspaceRoot.fsPath + path.sep)) {
// If the new workspace is a subfolder of the current workspace, do nothing
return;
}
const message = l10n.t('Would you like to open the created workspace?');
const open = l10n.t('Open');
const openNewWindow = l10n.t('Open in New Window');
const choices = [open, openNewWindow];
const result = await window.showInformationMessage(message, { modal: true }, ...choices);
if (result === open) {
await interactive.transferActiveChat(workspaceUri);
logService.info(
'[newIntent] Opening folder: ' + workspaceUri.fsPath,
);
commands.executeCommand('vscode.openFolder', workspaceUri);
} else if (result === openNewWindow) {
commands.executeCommand('vscode.openFolder', workspaceUri, true);
}
}
catch (error) {
const errorMessage = l10n.t('Failed to create workspace: {0}', projectName);
logService.error(error, errorMessage);
window.showErrorMessage(getSafeNotificationMessage(errorMessage, l10n.t('Failed to create workspace.')));
await workspace.fs.delete(workspaceUri, { recursive: true });
}
}
export function getFileCreationProgressMessage(file: Uri): string {
return getSafeNotificationMessage(l10n.t('Creating file {0}...', file.fsPath), l10n.t('Creating file...'));
}
/**
* Resolves the destination URI for a file emitted by `listFilesInResponseFileTree`
* inside a generated workspace.
*
* The emitted shape depends on the source flow:
* - Copilot new-workspace flow: `<projectName>/<inside>` (no leading slash).
* - GitHub repo-template flow: `/<inside>` (leading slash; the repo-name
* level has been collapsed out of the tree).
* - GitHub subdir/non-repo-root: `<subdir>/<inside>`.
*
* In every case the part *inside the project* is what we want to join with
* `workspaceUri`. We compute it by stripping any leading `/` and any leading
* `<projectRoot>/` prefix. Any remaining `..` segments are preserved so the
* downstream `isUriContained` check can reject traversal attempts.
*
* Using a string-strip (rather than the platform-aware `path.relative` against
* `cwd`) avoids the Windows-only failure mode where the relative `projectRoot`
* gets resolved against `process.cwd()` and produces a nonsense `..`-laden
* traversal that escapes the workspace.
*
* Exported for tests only.
*/
export function resolveProjectFileUri(workspaceUri: Uri, projectRoot: string, file: string): Uri {
let rel = file.startsWith('/') ? file.slice(1) : file;
const prefix = projectRoot + '/';
if (rel === projectRoot) {
rel = '';
} else if (rel.startsWith(prefix)) {
rel = rel.slice(prefix.length);
}
return Uri.joinPath(workspaceUri, rel);
}
/**
* Returns `true` if `child` resolves to a location at or within `parent`.
* Used to prevent path traversal (`..`) from escaping the generated workspace folder.
* Note: `Uri.joinPath` normalizes `..` segments, so `child.path` is already resolved here.
*
* Exported for tests only.
*/
export function isUriContained(parent: Uri, child: Uri): boolean {
if (parent.scheme !== child.scheme || parent.authority !== child.authority) {
return false;
}
if (child.path === parent.path) {
return true;
}
const parentPath = parent.path.endsWith('/') ? parent.path : parent.path + '/';
return child.path.startsWith(parentPath);
}
async function getUniqueProjectName(projectFolder: Uri, projectName: string): Promise<string> {
let i = 0;
let uniqueProjectNameNotFound = true;
let newProjectName = projectName.replace(/^\W+/, '');
while (uniqueProjectNameNotFound) {
try {
await workspace.fs.stat(Uri.joinPath(projectFolder, newProjectName));
newProjectName = projectName + '-' + ++i;
} catch {
uniqueProjectNameNotFound = false;
}
}
return newProjectName;
}
async function checkFileExists(filePath: Uri): Promise<boolean> {
try {
await workspace.fs.stat(filePath);
return true;
} catch (error) {
return false;
}
}
async function updateAiGeneratedWorkspacesFile(workspaceUris: Uri) {
const aiGeneratedFilePath = getAiGeneratedWorkspacesFile();
if (!aiGeneratedFilePath) {
return;
}
if ((await checkFileExists(aiGeneratedFilePath))) {
const fileContnet = await workspace.fs.readFile(aiGeneratedFilePath).then(b => { return new TextDecoder().decode(b); });
const workspaces = JSON.parse(fileContnet) as string[];
workspaces.push(workspaceUris.toString());
await workspace.fs.writeFile(aiGeneratedFilePath, Buffer.from(JSON.stringify(workspaces, null, 2)));
} else {
await workspace.fs.writeFile(aiGeneratedFilePath, Buffer.from(JSON.stringify([workspaceUris.toString()], null, 2)));
}
}
function getAiGeneratedWorkspacesFile(): Uri | undefined {
const vscodeFolderName = env.appName.indexOf('Insider') > 0 || env.appName.indexOf('Code - OSS Dev') >= 0 ? 'Code - Insiders' : 'Code';
const homeDir = Uri.file(process.env.HOME || (process.env.USERPROFILE ? process.env.USERPROFILE : ''));
switch (process.platform) {
case 'darwin':
return Uri.joinPath(
homeDir,
'Library',
'Application Support',
vscodeFolderName,
'User',
'workspaceStorage',
'aiGeneratedWorkspaces.json'
);
case 'linux':
return Uri.joinPath(homeDir, '.config', vscodeFolderName, 'User', 'workspaceStorage', 'aiGeneratedWorkspaces.json');
case 'win32':
return process.env.APPDATA
? Uri.joinPath(Uri.file(process.env.APPDATA), vscodeFolderName, 'User', 'workspaceStorage', 'aiGeneratedWorkspaces.json')
: undefined;
default:
return;
}
}
@@ -21,7 +21,6 @@ import { IInstantiationService } from '../../../../util/vs/platform/instantiatio
import { IMergeConflictService } from '../../../git/common/mergeConflictService';
import { TestMergeConflictServiceImpl } from '../../../git/vscode/mergeConflictServiceImpl';
import { IIntentService, IntentService } from '../../../intents/node/intentService';
import { INewWorkspacePreviewContentManager, NewWorkspacePreviewContentManagerImpl } from '../../../intents/node/newIntent';
import { createExtensionTestingServices } from '../../../test/vscode-node/services';
import { ConversationFeature } from '../conversationFeature';
@@ -33,7 +32,6 @@ suite('Conversation feature test suite', function () {
function createAccessor() {
const testingServiceCollection = createExtensionTestingServices();
testingServiceCollection.define(ICombinedEmbeddingIndex, new SyncDescriptor(VSCodeCombinedIndexImpl, [/*useRemoteCache*/ false]));
testingServiceCollection.define(INewWorkspacePreviewContentManager, new SyncDescriptor(NewWorkspacePreviewContentManagerImpl));
testingServiceCollection.define(IGitCommitMessageService, new NoopGitCommitMessageService());
testingServiceCollection.define(IDevContainerConfigurationService, new FailingDevContainerConfigurationService());
testingServiceCollection.define(IIntentService, new SyncDescriptor(IntentService));
@@ -1,121 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
import { expect, suite, test } from 'vitest';
import { getFileCreationProgressMessage, isUriContained, resolveProjectFileUri } from '../newWorkspaceFollowup';
suite('workspace creation notification security', () => {
test('preserves file targets but does not turn their names into progress links', () => {
const paths = [
'/workspace/ordinary [file].ts',
'/workspace/[Open](command:test.noop).ts',
'/workspace/[Open](CoMmAnD:test.noop?%5B%22arg%22%5D "Title").ts',
'/workspace/\\[Open\\](command:test.noop).ts',
'/workspace/[Help](file:private).ts',
'/workspace/COMMAND:test.noop.ts',
];
const files = paths.map(path => Uri.from({ scheme: 'file', path }));
expect(files.map(file => ({ path: file.path, message: getFileCreationProgressMessage(file) }))).toEqual(paths.map((path, index) => ({
path,
message: index === 0 ? `Creating file ${files[0].fsPath}...` : 'Creating file...',
})));
});
});
/**
* Unit tests for the path-traversal containment helper guarding the
* "Create Workspace" file-write loop. This is the runtime defense-in-depth
* for the MSRC-reported path traversal in the new-workspace flow.
*/
suite('isUriContained', () => {
const parent = Uri.file('/home/u/proj');
test('accepts paths at or within the parent folder', () => {
expect(isUriContained(parent, parent)).toBe(true);
expect(isUriContained(parent, Uri.file('/home/u/proj/a.txt'))).toBe(true);
expect(isUriContained(parent, Uri.file('/home/u/proj/sub/dir/a.txt'))).toBe(true);
});
test('rejects sibling paths with a shared name prefix', () => {
// Classic prefix-collision: /home/u/proj vs /home/u/proj-evil
expect(isUriContained(parent, Uri.file('/home/u/proj-evil/x'))).toBe(false);
expect(isUriContained(parent, Uri.file('/home/u/projx'))).toBe(false);
});
test('rejects paths that escape the parent via traversal', () => {
// `Uri.joinPath` normalizes `..` before the comparison runs, so we
// supply the resolved URI that the production code would actually see.
expect(isUriContained(parent, Uri.file('/home/u/package.json'))).toBe(false);
expect(isUriContained(parent, Uri.file('/etc/passwd'))).toBe(false);
});
test('rejects URIs with mismatched scheme or authority', () => {
const remoteParent = Uri.parse('vscode-remote://host/proj');
const localChild = Uri.file('/proj/a.txt');
expect(isUriContained(remoteParent, localChild)).toBe(false);
const otherHost = Uri.parse('vscode-remote://other/proj/a.txt');
expect(isUriContained(remoteParent, otherHost)).toBe(false);
const sameHostChild = Uri.parse('vscode-remote://host/proj/a.txt');
expect(isUriContained(remoteParent, sameHostChild)).toBe(true);
});
test('handles a parent path that already ends with a slash', () => {
const withSlash = parent.with({ path: parent.path + '/' });
expect(isUriContained(withSlash, Uri.file('/home/u/proj/a.txt'))).toBe(true);
expect(isUriContained(withSlash, Uri.file('/home/u/proj-evil/x'))).toBe(false);
});
});
/**
* Regression test for the GitHub repo-template "Create Workspace" flow.
*
* `listFilesInResponseFileTree` emits paths that may or may not include a
* leading `/` depending on the source (the GitHub repo-template flow emits
* `/<repo>/...`, the copilot flow emits `<projectName>/...`). The platform-
* aware `path.relative` previously resolved a relative `projectRoot` against
* `cwd` for the absolute `file`, producing a nonsense `..`-laden traversal that
* `Uri.joinPath` normalized outside `workspaceUri` — so the runtime containment
* guard added by the MSRC fix would silently skip every file. The fix forces
* posix semantics with absolutized inputs.
*/
suite('resolveProjectFileUri', () => {
const workspaceUri = Uri.file('/home/u/parent/myrepo');
test('handles a `/`-prefixed file path (GitHub repo-template flow)', () => {
const fileUri = resolveProjectFileUri(workspaceUri, 'myrepo', '/src/index.ts');
expect(fileUri.path).toBe('/home/u/parent/myrepo/src/index.ts');
});
test('handles a non-prefixed file path (copilot new-workspace flow)', () => {
const fileUri = resolveProjectFileUri(workspaceUri, 'myrepo', 'myrepo/src/index.ts');
expect(fileUri.path).toBe('/home/u/parent/myrepo/src/index.ts');
});
test('preserves traversal segments so the containment check can reject them', () => {
// A bypass of the parser must still be caught downstream by `isUriContained`.
// `Uri.joinPath` normalizes `..`, so the resolved path escapes the workspace
// and `isUriContained(workspaceUri, fileUri)` will return false.
const fileUri = resolveProjectFileUri(workspaceUri, 'myrepo', 'myrepo/../package.json');
expect(isUriContained(workspaceUri, fileUri)).toBe(false);
});
test('source preview URI built from the unmodified `file` resolves to the expected preview path', () => {
// `createWorkspace` reads preview content via `Uri.joinPath(baseUri, file)`
// using the *unmodified* `file` emitted by `listFilesInResponseFileTree`.
// `Uri.joinPath` is backed by `paths.posix.join`, which treats arguments
// as path segments (it does NOT drop `baseUri.path` when the next segment
// starts with `/`). Asserting this here so the source/destination URI
// shapes do not drift in the future.
const baseUri = Uri.parse('vscode-copilot-github-workspace://req/myrepo');
// GitHub repo-template flow: emitted file = `/src/index.ts`.
expect(Uri.joinPath(baseUri, '/src/index.ts').path).toBe('/myrepo/src/index.ts');
// Copilot new-workspace flow: emitted file = `myproject/src/index.ts`.
const copilotBase = Uri.parse('vscode-copilot-workspace://req/myproject');
expect(Uri.joinPath(copilotBase, 'myproject/src/index.ts').path).toBe('/myproject/myproject/src/index.ts');
});
});
@@ -29,7 +29,6 @@ import { RemoteAgentContribution } from '../../conversation/vscode-node/remoteAg
import { DiagnosticsContextContribution } from '../../diagnosticsContext/vscode/diagnosticsContextProvider';
import { LanguageModelProxyContrib } from '../../externalAgents/vscode-node/lmProxyContrib';
import { WalkthroughCommandContribution } from '../../getting-started/vscode-node/commands';
import * as newWorkspaceContribution from '../../getting-started/vscode-node/newWorkspace.contribution';
import { ScmContextProviderContribution } from '../../git/vscode/scmContextprovider';
import { GitHubMcpContrib } from '../../githubMcp/vscode-node/githubMcp.contribution';
import { IgnoredFileProviderContribution } from '../../ignore/vscode-node/ignoreProvider';
@@ -129,7 +128,6 @@ export const vscodeNodeChatContributions: IExtensionContributionFactory[] = [
asContributionFactory(McpSetupCommands),
asContributionFactory(LanguageModelProxyContrib),
asContributionFactory(PromptFileContribution),
newWorkspaceContribution,
asContributionFactory(OTelChatDebugLogProviderContribution),
asContributionFactory(ChatDebugFileLoggerContribution),
];
@@ -112,7 +112,6 @@ import { IUserFeedbackService, UserFeedbackService } from '../../conversation/vs
import { ConversationStore, IConversationStore } from '../../conversationStore/node/conversationStore';
import { SimilarFilesContextService } from '../../inlineEdits/vscode-node/similarFilesContext';
import { IIntentService, IntentService } from '../../intents/node/intentService';
import { INewWorkspacePreviewContentManager, NewWorkspacePreviewContentManagerImpl } from '../../intents/node/newIntent';
import { LanguageContextProviderService } from '../../languageContextProvider/vscode-node/languageContextProviderService';
import { ILinkifyService, LinkifyService } from '../../linkify/common/linkifyService';
import { DebugCommandToConfigConverter, IDebugCommandToConfigConverter } from '../../onboardDebug/node/commandToConfigConverter';
@@ -252,7 +251,6 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio
builder.define(IGithubApiFetcherService, new SyncDescriptor(GithubApiFetcherService));
builder.define(IAdoCodeSearchService, new SyncDescriptor(AdoCodeSearchService));
builder.define(ISettingsEditorSearchService, new SyncDescriptor(SettingsEditorSearchServiceImpl));
builder.define(INewWorkspacePreviewContentManager, new SyncDescriptor(NewWorkspacePreviewContentManagerImpl));
builder.define(IPromptVariablesService, new SyncDescriptor(PromptVariablesServiceImpl));
builder.define(IPromptWorkspaceLabels, new SyncDescriptor(PromptWorkspaceLabels));
builder.define(IUserFeedbackService, new SyncDescriptor(UserFeedbackService));
@@ -62,7 +62,6 @@ import { INotificationService, NullNotificationService } from '../../../platform
import { NotificationService } from '../../../platform/notification/vscode/notificationServiceImpl';
import { IUrlOpener, NullUrlOpener } from '../../../platform/open/common/opener';
import { RealUrlOpener } from '../../../platform/open/vscode/opener';
import { IProjectTemplatesIndex, ProjectTemplatesIndex } from '../../../platform/projectTemplatesIndex/common/projectTemplatesIndex';
import { IPromptPathRepresentationService, PromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
import { IReleaseNotesService } from '../../../platform/releaseNotes/common/releaseNotesService';
import { ReleaseNotesService } from '../../../platform/releaseNotes/vscode/releaseNotesServiceImpl';
@@ -151,7 +150,6 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio
builder.define(IMcpService, new SyncDescriptor(McpService));
builder.define(IExtensionsService, new SyncDescriptor(VSCodeExtensionsService));
builder.define(ICombinedEmbeddingIndex, new SyncDescriptor(VSCodeCombinedIndexImpl, [/*useRemoteCache*/ true]));
builder.define(IProjectTemplatesIndex, new SyncDescriptor(ProjectTemplatesIndex, [/*useRemoteCache*/ true]));
builder.define(IBlockedExtensionService, new SyncDescriptor(BlockedExtensionService));
builder.define(IEditLogService, new SyncDescriptor(EditLogService));
builder.define(IMultiFileEditInternalTelemetryService, new SyncDescriptor(MultiFileEditInternalTelemetryService));
@@ -1,28 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
export const NEW_WORKSPACE_STORAGE_KEY = 'copilot.newWorkspaceAgent.workspaceContexts';
export interface INewWorkspaceStoredData {
workspaceURI: string;
userPrompt: string;
initialized: boolean | undefined;
}
export function saveNewWorkspaceContext(add: INewWorkspaceStoredData, extensionContext: IVSCodeExtensionContext) {
const contexts = extensionContext.globalState.get<INewWorkspaceStoredData[]>(NEW_WORKSPACE_STORAGE_KEY, []);
const idx = contexts.findIndex(context => context.workspaceURI === add.workspaceURI);
if (idx >= 0) {
contexts.splice(idx, 1);
}
contexts.unshift(add);
while (contexts.length > 30) {
contexts.pop();
}
extensionContext.globalState.update(NEW_WORKSPACE_STORAGE_KEY, contexts);
}
@@ -1,17 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';
import { NewWorkspaceInitializer } from './newWorkspaceInitializer';
export function create(accessor: ServicesAccessor): IDisposable {
const instantiationService = accessor.get(IInstantiationService);
const disposableStore = new DisposableStore();
disposableStore.add(instantiationService.createInstance(NewWorkspaceInitializer));
return disposableStore;
}
@@ -1,74 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { l10n } from 'vscode';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { INewWorkspaceStoredData, NEW_WORKSPACE_STORAGE_KEY } from '../common/newWorkspaceContext';
export class NewWorkspaceInitializer extends Disposable {
constructor(
@IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext,
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
@IFileSystemService private readonly fileSystemService: IFileSystemService
) {
super();
this._updateWorkspace();
}
private async _updateWorkspace(): Promise<void> {
const workspace = this.workspaceService.getWorkspaceFolders();
if (!workspace || workspace.length === 0) {
return;
}
const newWorkspaceContextsList = this._extensionContext.globalState.get<INewWorkspaceStoredData[]>(NEW_WORKSPACE_STORAGE_KEY, []);
const exactIndex = newWorkspaceContextsList.findIndex(c => c.workspaceURI === workspace[0].toString());
if (exactIndex === -1) {
return;
}
const context = newWorkspaceContextsList[exactIndex];
const confirm = l10n.t('Continue Setup');
const message = l10n.t('Continue Workspace Setup?');
const detail = l10n.t('Copilot will resume setting up the workspace by creating the necessary files.');
if (!context.initialized) {
context.initialized = true;
newWorkspaceContextsList[exactIndex] = context;
this._extensionContext.globalState.update(NEW_WORKSPACE_STORAGE_KEY, newWorkspaceContextsList);
const result = await vscode.window.showInformationMessage(message, { modal: true, detail }, confirm);
if (result === confirm) {
vscode.commands.executeCommand('workbench.action.chat.open', { mode: 'agent', query: `${l10n.t('Continue with #new workspace setup')}` });
} else {
newWorkspaceContextsList.splice(exactIndex, 1);
this._extensionContext.globalState.update(NEW_WORKSPACE_STORAGE_KEY, newWorkspaceContextsList);
}
return;
}
const entries = await this.fileSystemService.readDirectory(workspace[0]);
const isEffectivelyEmpty = entries.every(([name]) => name === '.git');
if (!isEffectivelyEmpty) {
// workspace is not empty and we've already initialized it
newWorkspaceContextsList.splice(exactIndex, 1);
this._extensionContext.globalState.update(NEW_WORKSPACE_STORAGE_KEY, newWorkspaceContextsList);
} else {
// workspace is still empty, so ask to setup again
const result = await vscode.window.showInformationMessage(message, { modal: true, detail }, confirm);
if (result === confirm) {
vscode.commands.executeCommand('workbench.action.chat.open', { mode: 'agent', query: context.userPrompt });
} else {
newWorkspaceContextsList.splice(exactIndex, 1);
this._extensionContext.globalState.update(NEW_WORKSPACE_STORAGE_KEY, newWorkspaceContextsList);
}
}
}
}
@@ -13,7 +13,6 @@ import { EditCodeIntent } from './editCodeIntent';
import { ExplainIntent } from './explainIntent';
import { FixIntent } from './fixIntent';
import { GenerateCodeIntent } from './generateCodeIntent';
import { NewWorkspaceIntent } from './newIntent';
import { NewNotebookIntent } from './newNotebookIntent.contribution';
import { NotebookEditorIntent } from './notebookEditorIntent';
import { ReviewIntent } from './reviewIntent';
@@ -40,7 +39,6 @@ IntentRegistry.setIntents([
new SyncDescriptor(UnknownIntent),
new SyncDescriptor(GenerateCodeIntent),
new SyncDescriptor(NewNotebookIntent),
new SyncDescriptor(NewWorkspaceIntent),
new SyncDescriptor(VscodeIntent),
new SyncDescriptor(SetupTestsIntent),
new SyncDescriptor(SearchPanelIntent),
@@ -1,94 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { CancellationToken } from 'vscode';
import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
import { FileContentsPrompt, NewWorkspaceContentsPromptProps, ProjectSpecificationPrompt } from '../../prompts/node/panel/newWorkspace/newWorkspaceContents';
abstract class NewWorkspaceContentGenerator {
constructor(
private readonly promptType: typeof FileContentsPrompt | typeof ProjectSpecificationPrompt,
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }
public async generate(promptArgs: NewWorkspaceContentsPromptProps, token: CancellationToken): Promise<string> {
const endpoint = await this.endpointProvider.getChatEndpoint('copilot-utility-small');
const promptRenderer = PromptRenderer.create(this.instantiationService, endpoint, this.promptType, promptArgs);
const prompt = await promptRenderer.render();
const fetchResult = await endpoint
.makeChatRequest(
'newWorkspaceContentGenerator',
prompt.messages,
undefined,
token,
ChatLocation.Other,
undefined,
undefined,
);
return fetchResult.type === ChatFetchResponseType.Success ?
(promptArgs.filePath ? this.parseContents(promptArgs.filePath, fetchResult.value) : fetchResult.value) :
'';
}
protected abstract parseContents(filePath: string, chatResponse: string): string;
}
export class ProjectSpecificationGenerator extends NewWorkspaceContentGenerator {
constructor(
@IEndpointProvider endpointProvider: IEndpointProvider,
@IInstantiationService instantiationService: IInstantiationService
) {
super(ProjectSpecificationPrompt, endpointProvider, instantiationService);
}
protected override parseContents(chatResponse: string, filePath?: string | undefined): string {
throw new Error('Method not implemented.');
}
}
export class FileContentsGenerator extends NewWorkspaceContentGenerator {
constructor(
@IEndpointProvider endpointProvider: IEndpointProvider,
@IInstantiationService instantiationService: IInstantiationService,
) {
super(FileContentsPrompt, endpointProvider, instantiationService);
}
protected parseContents(filePath: string, chatResponse: string,): string {
function safeParse(str: string, regex: RegExp) {
try {
const match = regex.exec(str.trim());
if (match && match.length > 2) {
return match[2];
}
} catch (ex) {
console.error(ex);
}
return str;
}
if (filePath.endsWith('.md')) {
// If returned as a markdown codeblock, strip the codeblock markers
const fromCodeblock = safeParse(chatResponse, /^```([a-zA-Z]+)?\s*([\s\S]+?)\s*```$/);
// If returned as bare text, remove any text before the first header
const [preamble, ...withoutPreamble] = fromCodeblock.split('#');
if (preamble.length) {
return ['', ...withoutPreamble].join('#');
}
return fromCodeblock;
} else {
return safeParse(chatResponse, /```([^\n]+)?\s*\n([\s\S]+?)\s*```/g);
}
}
}
@@ -1,623 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as l10n from '@vscode/l10n';
import { Raw } from '@vscode/prompt-tsx';
import { parse } from 'jsonc-parser';
import type * as vscode from 'vscode';
import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';
import { ChatLocation } from '../../../platform/chat/common/commonTypes';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
import { FileType } from '../../../platform/filesystem/common/fileTypes';
import { GithubRepositoryItem, IGithubRepositoryService } from '../../../platform/github/common/githubService';
import { IChatEndpoint } from '../../../platform/networking/common/networking';
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
import { extractCodeBlocks } from '../../../util/common/markdown';
import { createServiceIdentifier } from '../../../util/common/services';
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
import * as path from '../../../util/vs/base/common/path';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { ChatResponseFileTreePart, MarkdownString, Uri } from '../../../vscodeTypes';
import { Intent } from '../../common/constants';
import { commandUri } from '../../linkify/common/commands';
import { convertFileTreeToChatResponseFileTree } from '../../prompt/common/fileTreeParser';
import { IBuildPromptContext } from '../../prompt/common/intents';
import { IIntent, IIntentInvocation, IIntentInvocationContext, IIntentSlashCommandInfo, IntentLinkificationOptions, IResponseProcessorContext } from '../../prompt/node/intents';
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
import { NewWorkspaceGithubContentMetadata, NewWorkspacePrompt } from '../../prompts/node/panel/newWorkspace/newWorkspace';
import { NewWorkspaceContentsPromptProps } from '../../prompts/node/panel/newWorkspace/newWorkspaceContents';
import { FileContentsGenerator, ProjectSpecificationGenerator } from './generateNewWorkspaceContent';
interface FileTreeDataWithContent extends vscode.ChatResponseFileTree {
content?: Promise<Uint8Array | undefined>;
ctime?: number;
type?: FileType;
}
export const INewWorkspacePreviewContentManager = createServiceIdentifier<INewWorkspacePreviewContentManager>('INewWorkspacePreviewContentManager');
export interface INewWorkspacePreviewContentManager {
readonly _serviceBrand: undefined;
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any): void;
get(uri: Uri): FileTreeDataWithContent | undefined;
getFileTree(responseId: string): ChatResponseFileTreePart | undefined;
}
export const CreateProjectCommand = 'github.copilot.createProject';
export const CreateFileCommand = 'github.copilot.createFile';
export const OpenFileCommand = 'github.copilot.openFile';
export class NewWorkspacePreviewContentManagerImpl implements INewWorkspacePreviewContentManager {
declare readonly _serviceBrand: undefined;
private readonly copilotContentManager: NewWorkspaceCopilotContentManager;
private readonly githubContentManager: NewWorkspaceGitHubContentManager;
private readonly fileContentManager: NewWorkspaceFileContentManager;
private responseScopedData = new Map<string, ChatResponseFileTreePart>();
private prevResponseId: string | undefined;
private prevFileContents = new Map<string, string>();
constructor(
@IInstantiationService instantiationService: IInstantiationService,
) {
this.copilotContentManager = instantiationService.createInstance(NewWorkspaceCopilotContentManager);
this.githubContentManager = instantiationService.createInstance(NewWorkspaceGitHubContentManager);
this.fileContentManager = new NewWorkspaceFileContentManager();
}
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {
this.responseScopedData.set(responseId, fileTree);
if (isGithubWorkspaceUri(fileTree.baseUri)) {
this.githubContentManager.set(responseId, projectName, fileTree, serviceArgs);
} else if (isCopiltoFileWorkspaceUri(fileTree.baseUri)) {
this.fileContentManager.set(responseId, projectName, fileTree, serviceArgs);
} else {
this.copilotContentManager.set(responseId, projectName, fileTree, serviceArgs);
}
}
get(uri: Uri): FileTreeDataWithContent | undefined {
if (this.prevResponseId !== uri.authority) {
this.prevFileContents.clear();
this.prevResponseId = uri.authority;
}
let fileContents: FileTreeDataWithContent | undefined;
if (isGithubWorkspaceUri(uri)) {
fileContents = this.githubContentManager.get(uri.authority, uri.path);
} else if (isCopiltoFileWorkspaceUri(uri)) {
fileContents = this.fileContentManager.get(uri.authority, uri.path);
} else {
fileContents = this.copilotContentManager.get(uri.authority, uri.path, this.prevFileContents);
}
fileContents?.content?.then((content) => {
if (this.prevFileContents.has(uri.path)) {
return;
}
const decoder = new TextDecoder();
const fileContentStr = decoder.decode(content);
this.prevFileContents.set(uri.path, fileContentStr);
});
return fileContents;
}
getFileTree(responseId: string): ChatResponseFileTreePart | undefined {
return this.responseScopedData.get(responseId);
}
}
interface ProjectData {
userPrompt: string;
projectStructure: string;
projectSpecification: Promise<string>;
fileTree: ChatResponseFileTreePart;
chatMessages: Raw.ChatMessage[];
}
export class NewWorkspaceCopilotContentManager {
declare readonly _serviceBrand: undefined;
private promises: Promise<unknown>[] = [];
private responseScopedData = new Map<string, Map<string, ProjectData>>();
private generatePlanPrompt = this.instantiationService.createInstance(ProjectSpecificationGenerator);
private generateFilePrompt = this.instantiationService.createInstance(FileContentsGenerator);
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }
// TODO@joyceerhl persistence between reloads
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {
const { userPrompt, projectStructure, chatMessages } = serviceArgs;
const promptArgs: NewWorkspaceContentsPromptProps = {
query: userPrompt,
fileTreeStr: projectStructure,
history: chatMessages
};
const projectSpecificationPromise = this.generatePlanPrompt.generate(promptArgs, CancellationToken.None);
this.promises.push(projectSpecificationPromise);
const sessionScopedData = this._getResponseScopedData(responseId);
const projectData: ProjectData = { userPrompt, projectSpecification: projectSpecificationPromise, projectStructure, fileTree: fileTree, chatMessages };
sessionScopedData.set(projectName, projectData);
}
get(responseId: string, path: string, prevFileContents: Map<string, string>): FileTreeDataWithContent | undefined {
const { projectName, path: relativePath } = this._getProjectMetadata(path);
const responseScopedData = this._getResponseScopedData(responseId);
const data = responseScopedData.get(projectName);
if (!data) {
return;
}
const fileNodes: FileTreeDataWithContent[] = data.fileTree.value;
const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);
if (currentNode && !currentNode?.content) {
const nodeWithMissingContent = currentNode;
nodeWithMissingContent.content = this._getFileContent(data.userPrompt, data.projectStructure, data.projectSpecification, path, data.chatMessages, prevFileContents).catch(() => nodeWithMissingContent.content = undefined);
}
return currentNode;
}
private _prefetch(userPrompt: string, projectStructure: string, projectSpecification: Promise<string>, fileTree: vscode.ChatResponseFileTree, chatMessages: Raw.ChatMessage[]): FileTreeDataWithContent {
const ctime = Date.now();
if (fileTree.children) {
return { ...fileTree, type: FileType.Directory, children: fileTree.children.map((child) => this._prefetch(userPrompt, projectStructure, projectSpecification, child, chatMessages)), ctime };
}
// Disable prefetching for now
// node.content = this._getFileContent(userPrompt, projectStructure, projectSpecification, fileTreeData.uri.path, chatMessages).catch(() => node.content = undefined);
return { ...fileTree, type: FileType.File, content: undefined, ctime };
}
private async _getFileContent(projectDescription: string, projectStructure: string, projectSpecPromise: Promise<string>, filePath: string, chatMessages: Raw.ChatMessage[], prevFileContents: Map<string, string>): Promise<Uint8Array> {
const promptArgs: NewWorkspaceContentsPromptProps = {
query: projectDescription,
fileTreeStr: projectStructure,
filePath: filePath,
projectSpecification: await projectSpecPromise,
history: chatMessages,
relavantFiles: prevFileContents.has(filePath) ? new Map([[filePath, prevFileContents.get(filePath)!]]) : undefined
};
return this.generateFilePrompt.generate(promptArgs, CancellationToken.None).then((response) => Buffer.from(response));
}
private _getResponseScopedData(responseId: string) {
let responseScopedData = this.responseScopedData.get(responseId);
if (!responseScopedData) {
responseScopedData = new Map<string, ProjectData>();
this.responseScopedData.set(responseId, responseScopedData);
}
return responseScopedData;
}
private _getProjectMetadata(fullPath: string) {
// Format: vscode-copilot-workspace://<sessionId>/<projectName>/<filePath>
const [, projectName, ...path] = fullPath.split('/');
return { projectName, path };
}
}
interface GithubData {
org: string;
repo: string;
path: string;
fileTree: ChatResponseFileTreePart;
}
class NewWorkspaceGitHubContentManager {
private responseScopedData = new Map<string, Map<string, GithubData>>();
constructor(
@IGithubRepositoryService private readonly repositoryService: IGithubRepositoryService
) { }
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {
const githubContentMetadata = serviceArgs as NewWorkspaceGithubContentMetadata;
const sessionScopedData = this._getResponseScopedData(responseId);
const githubData: GithubData = { ...githubContentMetadata, fileTree };
sessionScopedData.set(projectName, githubData);
}
get(responseId: string, filePath: string): FileTreeDataWithContent | undefined {
const { projectName, path: relativePath } = this._getProjectMetadata(filePath);
const responseScopedData = this._getResponseScopedData(responseId);
const rootNode = responseScopedData.get(projectName);
if (!rootNode) {
return;
}
const fileNodes: FileTreeDataWithContent[] = rootNode.fileTree.value;
const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);
if (currentNode && !currentNode?.content && !currentNode?.children) {
const nodeWithMissingContent = currentNode;
const folderPath = rootNode.path === '.' ? path.posix.relative(rootNode.repo, filePath) : path.posix.relative(rootNode.path, filePath.slice(1));
nodeWithMissingContent.content = this.repositoryService.getRepositoryItemContent(rootNode.org, rootNode.repo, folderPath).catch(() => nodeWithMissingContent.content = undefined);
}
return currentNode;
}
private _getProjectMetadata(fullPath: string) {
// Format: vscode-copilot-github-workspace://<sessionId>/<projectName>/<filePath>
const [, projectName, ...path] = fullPath.split('/');
return { projectName, path };
}
private _getResponseScopedData(responseId: string) {
let responseScopedData = this.responseScopedData.get(responseId);
if (!responseScopedData) {
responseScopedData = new Map<string, GithubData>();
this.responseScopedData.set(responseId, responseScopedData);
}
return responseScopedData;
}
}
interface FileData {
fileTree: ChatResponseFileTreePart;
content: string;
}
class NewWorkspaceFileContentManager {
private responseScopedData = new Map<string, Map<string, FileData>>();
constructor() {
}
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {
const fileContents = serviceArgs as string;
const sessionScopedData = this._getResponseScopedData(responseId);
const fileContentData: FileData = { content: fileContents, fileTree };
sessionScopedData.set(projectName, fileContentData);
}
get(responseId: string, filePath: string): FileTreeDataWithContent | undefined {
const { projectName, path: relativePath } = this._getFileMetadata(filePath);
const responseScopedData = this._getResponseScopedData(responseId);
const rootNode = responseScopedData.get(projectName);
if (!rootNode) {
return;
}
const fileNodes: FileTreeDataWithContent[] = rootNode.fileTree.value;
const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);
if (currentNode && !currentNode?.content && !currentNode?.children) {
currentNode.content = Promise.resolve(new Uint8Array(new TextEncoder().encode(rootNode.content)));
}
return currentNode;
}
private _getFileMetadata(fullPath: string) {
// Format: vscode-copilot-file://<sessionId>/<projectName>/<filePath>
const [, projectName, ...path] = fullPath.split('/');
return { projectName, path };
}
private _getResponseScopedData(responseId: string) {
let responseScopedData = this.responseScopedData.get(responseId);
if (!responseScopedData) {
responseScopedData = new Map<string, FileData>();
this.responseScopedData.set(responseId, responseScopedData);
}
return responseScopedData;
}
}
function findMatchingNodeFromPath(fileTree: vscode.ChatResponseFileTree[], pathElements: string[]): FileTreeDataWithContent | undefined {
let currentNode: FileTreeDataWithContent | undefined = undefined;
for (const element of pathElements) {
if (currentNode) {
if (currentNode.children) {
currentNode = currentNode.children.find(node => node.name === element) ?? currentNode;
}
} else {
currentNode = fileTree.find(node => node.name === element);
}
}
return currentNode;
}
export const newId = 'new';
export class NewWorkspaceIntent implements IIntent {
static readonly ID = Intent.New;
readonly id: string = Intent.New;
readonly locations = [ChatLocation.Panel];
readonly description: string = l10n.t('Scaffold code for a new file or project in a workspace');
readonly commandInfo: IIntentSlashCommandInfo = {
allowsEmptyArgs: false,
defaultEnablement: true,
};
constructor(
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }
async invoke(invocationContext: IIntentInvocationContext): Promise<IIntentInvocation> {
const location = invocationContext.location;
const endpoint = await this.endpointProvider.getChatEndpoint(invocationContext.request);
return this.instantiationService.createInstance(NewWorkspaceIntentInvocation, this, endpoint, location);
}
}
function createProjectCommand(fileTree: ChatResponseFileTreePart, workspaceRoot: Uri | undefined): vscode.Command {
return {
command: CreateProjectCommand,
arguments: [fileTree, workspaceRoot],
title: l10n.t('Create Workspace...'),
};
}
function createFileCommand(fileTree: ChatResponseFileTreePart): vscode.Command {
return {
command: CreateFileCommand,
arguments: [fileTree],
title: l10n.t('Create File...'),
};
}
export class NewWorkspaceIntentInvocation implements IIntentInvocation {
private githubContentMetadata?: NewWorkspaceGithubContentMetadata;
readonly linkification: IntentLinkificationOptions = { disable: true };
constructor(
readonly intent: NewWorkspaceIntent,
readonly endpoint: IChatEndpoint,
readonly location: ChatLocation,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@INewWorkspacePreviewContentManager private readonly newWorkspacePreviewContentManager: INewWorkspacePreviewContentManager,
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
) { }
async getShouldUseProjectTemplate() {
const useProjectTemplates = this.configurationService.getConfig(ConfigKey.UseProjectTemplates);
if (useProjectTemplates !== undefined) {
return useProjectTemplates;
}
return false;
}
async buildPrompt(promptContext: IBuildPromptContext, progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart>, token: vscode.CancellationToken) {
// TODO: @bhavyaus enable using project templates with variables
const { query, history, chatVariables } = promptContext;
const useTemplates = !chatVariables.hasVariables() && history[history.length - 1]?.request?.message !== query && await this.getShouldUseProjectTemplate();
const renderer = PromptRenderer.create(this.instantiationService, this.endpoint, NewWorkspacePrompt, {
promptContext,
useTemplates: useTemplates,
endpoint: this.endpoint,
});
const result = await renderer.render(progress, token);
const metadata = result.metadata.get(NewWorkspaceGithubContentMetadata);
if (metadata) {
this.githubContentMetadata = metadata;
}
return result;
}
processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: CancellationToken): Promise<void> {
const responseProcessor = new NewWorkspaceResponseProcessor(this.newWorkspacePreviewContentManager, this.workspaceService, this.githubContentMetadata);
return responseProcessor.processResponse(context, inputStream, outputStream, token);
}
}
function convertGitHubItemsToChatResponseFileTree(items: GithubRepositoryItem[], baseUri: Uri, isRepoRoot: boolean): ChatResponseFileTreePart {
let paths: string[];
if (isRepoRoot) {
paths = items.map(item => [baseUri.path, item.path].join('/'));
} else {
paths = items.map(item => item.path);
}
const rootName = paths[0].split('/')[0];
const root: vscode.ChatResponseFileTree = { name: rootName, children: [] };
const result: { [key: string]: vscode.ChatResponseFileTree } = { rootName: root };
for (const path of paths) {
const pathParts = path.split('/');
let currentPath = rootName;
let currentNode = root;
for (let i = 1; i < pathParts.length; i++) {
const pathPart = pathParts[i];
currentPath += `/${pathPart}`;
if (!result[currentPath]) {
const newNode: vscode.ChatResponseFileTree = { name: pathPart };
if (currentNode.children === undefined) {
currentNode.children = [];
}
currentNode.children.push(newNode);
result[currentPath] = newNode;
}
currentNode = result[currentPath];
}
}
let baseTree: vscode.ChatResponseFileTree[];
if (isRepoRoot) {
baseTree = root.children?.[0].children ?? [];
} else {
baseTree = root.children ?? [];
}
const sortedTree = baseTree?.sort((a, b) => (a.children && !b.children) ? -1 : 1) ?? [];
return new ChatResponseFileTreePart([{ name: rootName, children: sortedTree }], baseUri);
}
export const CopilotWorkspaceScheme = 'vscode-copilot-workspace';
export const GithubWorkspaceScheme = 'vscode-copilot-github-workspace';
export const CopilotFileScheme = 'vscode-copilot-file';
function getNewPreviewUri(requestId: string | undefined, filePath?: string, isGithubRepo: boolean = false,) {
return Uri.from({
scheme: isGithubRepo ? GithubWorkspaceScheme : CopilotWorkspaceScheme,
authority: requestId ?? '',
path: filePath ? `/${filePath}` : undefined
});
}
class NewWorkspaceResponseProcessor {
private _appliedText = '';
private _p = Promise.resolve('');
constructor(
private readonly newWorkspacePreviewContentManager: INewWorkspacePreviewContentManager,
private readonly workspaceService: IWorkspaceService,
private readonly githubContentMetadata?: NewWorkspaceGithubContentMetadata
) { }
async processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<void> {
const { turn, messages } = context;
let isBufferingFileTree = false;
let projectStructure = '';
const fileTreeStartRegex = /```filetree\n/;
const chatMessages = messages.filter(message => message.role !== Raw.ChatRole.System); // Exclude system messages as we want to use a different identity for the additional prompts we run
let hasReportingStarted = false;
for await (const { delta } of inputStream) {
if (token.isCancellationRequested) {
break;
}
const incomingText = delta.text;
this._p = this._p.then(async (): Promise<string> => {
const requestId = turn.id;
if (!incomingText) {
return this._appliedText;
}
this._appliedText += incomingText;
if (!this._appliedText.startsWith('#')) {
const userPrompt = turn.request.message;
const hasWholeCodeBlock = this._appliedText.match(/```filetree\n([\s\S]+?)\n```/);
if (hasWholeCodeBlock && (isBufferingFileTree || !hasReportingStarted)) {
isBufferingFileTree = false;
const [before, after] = this._appliedText.split(hasWholeCodeBlock[0]);
if (!hasReportingStarted) {
// We have the whole codeblock but we haven't started reporting yet.
// This only happens in test when the entire response is in the incomingText.
outputStream.markdown(before);
}
projectStructure = hasWholeCodeBlock[1];
const { chatResponseTree, projectName } = convertFileTreeToChatResponseFileTree(projectStructure, fp => getNewPreviewUri(requestId, fp));
outputStream.progress(l10n.t('Generating workspace preview...'));
outputStream.push(chatResponseTree);
outputStream.markdown(after);
this.newWorkspacePreviewContentManager.set(requestId, projectName, chatResponseTree, { userPrompt, projectStructure, chatMessages });
} else if ((this._appliedText.match(fileTreeStartRegex)) && !isBufferingFileTree && !hasWholeCodeBlock) {
isBufferingFileTree = true;
const [_, after] = this._appliedText.split(fileTreeStartRegex);
projectStructure += after;
outputStream.progress(l10n.t('Generating workspace preview...'));
} else if (isBufferingFileTree) {
projectStructure += incomingText;
} else if (!isBufferingFileTree && (!this._appliedText.match(/```/))) {
hasReportingStarted = true;
outputStream.markdown(incomingText);
}
} else if (/(?:.*\n){1,}/.test(this._appliedText)) {
outputStream.markdown(incomingText);
}
return this._appliedText;
});
}
await this._p;
if (turn.id &&
this.githubContentMetadata &&
this.githubContentMetadata.org &&
this.githubContentMetadata.repo &&
this.githubContentMetadata.path &&
this.githubContentMetadata.githubRepoItems &&
!this.newWorkspacePreviewContentManager.getFileTree(turn.id)) {
outputStream.reference(Uri.parse(this.githubContentMetadata.githubRepoItems[0].html_url));
outputStream.progress(l10n.t('Generating workspace preview...'));
const isRepoRoot = this.githubContentMetadata.path === '.';
const projectName = isRepoRoot ? this.githubContentMetadata.repo : this.githubContentMetadata.path.split('/')[0];
const chatResponseTree = convertGitHubItemsToChatResponseFileTree(this.githubContentMetadata.githubRepoItems, getNewPreviewUri(turn.id, projectName, true), isRepoRoot);
outputStream.push(chatResponseTree);
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
outputStream.button(createProjectCommand(chatResponseTree, workspaceFolders.length > 0 ? workspaceFolders[0] : undefined));
this.newWorkspacePreviewContentManager.set(turn.id, projectName, chatResponseTree, this.githubContentMetadata);
const query = encodeURIComponent(`["/${newId} ${turn.request.message}"]`);
const markdownString = new MarkdownString(l10n.t(`Hint: You can [regenerate this project without using this sample](command:workbench.action.chat.open?{0}) or use this [setting](command:workbench.action.openSettings?%5B%22github.copilot.chat.useProjectTemplates%22%5D) to configure the behavior.`, query));
markdownString.isTrusted = { enabledCommands: ['workbench.action.openSettings', 'workbench.action.chat.open'] };
outputStream.markdown(markdownString);
}
else {
const fileContentGeneration = extractCodeBlocks(this._appliedText);
if (fileContentGeneration.length === 2) {
let fileName;
try {
fileName = parse(fileContentGeneration[1].code);
} catch (e) {
throw e;
}
const baseUri = Uri.from({
scheme: CopilotFileScheme,
authority: turn.id,
path: `/${fileName.fileName}`
});
const fileTree = new ChatResponseFileTreePart([{ name: `${fileName.fileName}` }], baseUri);
const commandstr = commandUri(OpenFileCommand, [fileTree]);
const markdownString = new MarkdownString(`[${fileName.fileName}](${commandstr})`);
markdownString.isTrusted = { enabledCommands: [OpenFileCommand] };
outputStream.markdown(l10n.t('Sure, here is the file you requested:'));
outputStream.markdown(markdownString);
this.newWorkspacePreviewContentManager.set(turn.id, fileName.fileName, fileTree, fileContentGeneration[0].code);
}
}
this.pushCommands(turn.id, outputStream);
}
pushCommands(turnRequestId: string, outputStream: vscode.ChatResponseStream): void {
// Extract the Repo structure here
const fileTree = this.newWorkspacePreviewContentManager.getFileTree(turnRequestId);
if (!fileTree) {
return;
}
if (isGithubWorkspaceUri(fileTree.baseUri)) {
return;
}
else if (isCopiltoFileWorkspaceUri(fileTree.baseUri)) {
outputStream.button(createFileCommand(fileTree));
return;
}
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
outputStream.button(createProjectCommand(fileTree, workspaceFolders.length > 0 ? workspaceFolders[0] : undefined));
}
}
function isGithubWorkspaceUri(uri: Uri): boolean {
return uri.scheme === GithubWorkspaceScheme;
}
function isCopiltoFileWorkspaceUri(uri: Uri): boolean {
return uri.scheme === CopilotFileScheme;
}
@@ -1,75 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, EventEmitter, FileChangeEvent, FileStat, FileSystemError, FileSystemProvider, FileType, Uri } from 'vscode';
import { INewWorkspacePreviewContentManager } from '../node/newIntent';
export class NewWorkspacePreviewFileSystemProvider implements FileSystemProvider {
constructor(private readonly contentManager: INewWorkspacePreviewContentManager) { }
async stat(uri: Uri): Promise<FileStat> {
const node = this.contentManager.get(uri);
if (!node) {
throw FileSystemError.FileNotFound(uri);
}
const size = await node.content?.then((content) => content?.length) ?? 0;
return {
ctime: node.ctime ?? 0,
mtime: node.ctime ?? 0,
size: size,
type: node.children ? FileType.Directory : FileType.File
};
}
readDirectory(uri: Uri): [string, FileType][] | Thenable<[string, FileType][]> {
const node = this.contentManager.get(uri);
if (!node) {
throw FileSystemError.FileNotFound(uri);
}
return node.children?.map((child) => [child.name, child.children ? FileType.Directory : FileType.File]) ?? [];
}
async readFile(uri: Uri): Promise<Uint8Array> {
const node = this.contentManager.get(uri);
if (!node) {
throw FileSystemError.FileNotFound(uri);
}
let content: Uint8Array | undefined;
try {
content = await node.content;
} catch { }
return content ?? new Uint8Array();
}
// #region not implemented since this filesystem impl is readonly
private readonly _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
onDidChangeFile: Event<FileChangeEvent[]> = this._onDidChangeFile.event;
watch(uri: Uri, options: { readonly recursive: boolean; readonly excludes: readonly string[] }): Disposable {
return { dispose() { } };
}
createDirectory(uri: Uri): void | Thenable<void> {
throw FileSystemError.NoPermissions(uri);
}
writeFile(uri: Uri, content: Uint8Array, options: { readonly create: boolean; readonly overwrite: boolean }): void | Thenable<void> {
throw FileSystemError.NoPermissions(uri);
}
delete(uri: Uri, options: { readonly recursive: boolean }): void | Thenable<void> {
throw FileSystemError.NoPermissions(uri);
}
rename(oldUri: Uri, newUri: Uri, options: { readonly overwrite: boolean }): void | Thenable<void> {
throw FileSystemError.NoPermissions(newUri);
}
copy?(source: Uri, destination: Uri, options: { readonly overwrite: boolean }): void | Thenable<void> {
throw FileSystemError.NoPermissions(destination);
}
// #endregion
dispose(): void {
this._onDidChangeFile.dispose();
}
}
@@ -1,34 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, EventEmitter, TextDocumentContentProvider, Uri, workspace } from 'vscode';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { CopilotFileScheme, INewWorkspacePreviewContentManager } from '../node/newIntent';
export class NewWorkspaceTextDocumentProvider extends Disposable implements TextDocumentContentProvider {
onDidChangeEmitter = this._register(new EventEmitter<Uri>());
public onDidChange = this.onDidChangeEmitter.event;
constructor(private readonly contentManager: INewWorkspacePreviewContentManager) {
super();
this._register(workspace.onDidChangeTextDocument(e => {
if (e.document.uri.scheme === CopilotFileScheme) {
this.onDidChangeEmitter.fire(e.document.uri);
}
}));
}
async provideTextDocumentContent(uri: Uri, token: CancellationToken) {
const node = this.contentManager.get(uri);
if (!node) {
return '';
}
let contentArray: Uint8Array | undefined;
try {
contentArray = await node.content;
} catch { }
return new TextDecoder().decode(contentArray) ?? '';
}
}
@@ -1,59 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { expect, suite, test } from 'vitest';
import { URI } from '../../../../util/vs/base/common/uri';
import { convertFileTreeToChatResponseFileTree, listFilesInResponseFileTree } from '../../common/fileTreeParser';
/**
* Regression test for the "Create Workspace" path-traversal vulnerability.
*
* A malicious or prompt-injected model response could embed `..` segments in the
* generated markdown file tree. When the tree was copied into the user-selected
* parent folder, `path.relative` + `Uri.joinPath` resolved those segments and let
* the write escape the generated workspace folder (e.g. overwriting a sibling's
* `package.json`).
*
* The parser now drops any node whose name is a traversal/separator segment, so
* the escaping path is never produced. (A second, independent containment check
* in `createWorkspace` guards the actual file write as defense in depth.)
*/
suite('newWorkspace file tree traversal (PoC)', () => {
const generatePreviewURI = (filename: string) => URI.file(`/preview/${filename}`);
test('parser does not emit traversal paths from a malicious tree', () => {
// The PoC tree embeds a `..` directory that points at the parent folder.
const fileStructure = `
project
├── package.json
├── ..
│ └── package.json
└── safe.txt
`;
const { chatResponseTree } = convertFileTreeToChatResponseFileTree(fileStructure, generatePreviewURI);
const files = listFilesInResponseFileTree(chatResponseTree.value);
// No produced path may contain a `..` (or other) traversal segment.
for (const file of files) {
expect(file.split('/')).not.toContain('..');
expect(file.split('/')).not.toContain('.');
}
// The legitimate, in-tree files are still produced.
expect(files).toContain('project/package.json');
expect(files).toContain('project/safe.txt');
});
test('parser rejects a malicious project root name', () => {
const fileStructure = `
..
├── package.json
└── safe.txt
`;
expect(() => convertFileTreeToChatResponseFileTree(fileStructure, generatePreviewURI)).toThrow();
});
});
@@ -217,7 +217,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -218,7 +218,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -217,7 +217,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -218,7 +218,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -170,7 +170,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -172,7 +172,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -217,7 +217,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -218,7 +218,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -170,7 +170,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -172,7 +172,6 @@ Available deferred tools (must be loaded with tool_search before use):
copilot_getNotebookSummary
create_directory
create_new_jupyter_notebook
create_new_workspace
edit_notebook_file
get_vscode_api
github_repo
@@ -1,477 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as l10n from '@vscode/l10n';
import { BasePromptElementProps, PromptElement, PromptPiece, PromptSizing, RenderPromptResult, SystemMessage, UserMessage } from '@vscode/prompt-tsx';
import type * as vscode from 'vscode';
import { ChatFetchResponseType, ChatLocation } from '../../../../../platform/chat/common/commonTypes';
import { EmbeddingType, IEmbeddingsComputer } from '../../../../../platform/embeddings/common/embeddingsComputer';
import { IEndpointProvider } from '../../../../../platform/endpoint/common/endpointProvider';
import { GithubRepositoryItem, IGithubRepositoryService } from '../../../../../platform/github/common/githubService';
import { IChatEndpoint } from '../../../../../platform/networking/common/networking';
import { IProjectTemplatesIndex } from '../../../../../platform/projectTemplatesIndex/common/projectTemplatesIndex';
import { reportProgressOnSlowPromise } from '../../../../../util/common/progress';
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
import * as path from '../../../../../util/vs/base/common/path';
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
import { ChatResponseProgressPart } from '../../../../../vscodeTypes';
import { newId } from '../../../../intents/node/newIntent';
import { PromptMetadata } from '../../../../prompt/common/conversation';
import { IBuildPromptContext } from '../../../../prompt/common/intents';
import { CopilotIdentityRules } from '../../base/copilotIdentity';
import { InstructionMessage } from '../../base/instructionMessage';
import { PromptRenderer } from '../../base/promptRenderer';
import { ResponseTranslationRules } from '../../base/responseTranslationRules';
import { SafetyRules } from '../../base/safetyRules';
import { ChatToolReferences, ChatVariablesAndQuery } from '../chatVariables';
import { ConversationHistory, HistoryWithInstructions } from '../conversationHistory';
import { CustomInstructions } from '../customInstructions';
import { EditorIntegrationRules } from '../editorIntegrationRules';
import { UnsafeCodeBlock } from '../unsafeElements';
export interface NewWorkspacePromptProps extends BasePromptElementProps {
promptContext: IBuildPromptContext;
useTemplates: boolean;
endpoint: IChatEndpoint;
}
export interface NewWorkspacePromptState {
intent?: Instruction;
url?: string;
}
type Instruction = {
intent: string;
question: string;
};
function parseInstruction(input: string): Instruction {
const lines = input.split('\n');
const instruction: Partial<Instruction> = {};
lines.forEach((line, index) => {
if (line.startsWith('# Intent')) {
instruction.intent = lines[index + 1]?.trim();
} else if (line.startsWith('# Question')) {
instruction.question = lines[index + 1]?.trim();
}
});
return instruction as Instruction;
}
export class NewWorkspacePrompt extends PromptElement<NewWorkspacePromptProps, NewWorkspacePromptState> {
private _metadata: NewWorkspaceGithubContentMetadata | undefined;
constructor(props: NewWorkspacePromptProps,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IEmbeddingsComputer private readonly embeddingsComputer: IEmbeddingsComputer,
@IEndpointProvider private readonly endPointProvider: IEndpointProvider,
@IProjectTemplatesIndex private readonly projectTemplatesIndex: IProjectTemplatesIndex,
@IGithubRepositoryService private readonly repositoryService: IGithubRepositoryService,
) {
super(props);
}
override async prepare(sizing: PromptSizing, progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart> | undefined, token: CancellationToken): Promise<NewWorkspacePromptState> {
if (!progress) {
throw new Error('Progress is required');
}
progress?.report(new ChatResponseProgressPart(l10n.t('Determining user intent...')));
const endpoint = await this.endPointProvider.getChatEndpoint('copilot-utility-small');
const { messages } = await buildNewWorkspaceMetaPrompt(this.instantiationService, endpoint, this.props.promptContext);
if (token.isCancellationRequested) {
return {};
}
const fetchResult = await endpoint.makeChatRequest(
'newWorkspace',
messages,
undefined,
token,
ChatLocation.Panel,
undefined,
{
temperature: 0,
},
);
if (fetchResult.type === ChatFetchResponseType.Success) {
const instruction = parseInstruction(fetchResult.value);
if (instruction.intent === 'File') {
return { intent: instruction };
}
else if (instruction.intent === 'Project') {
if (this.props.useTemplates) {
const result = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.metis_1024_I16_Binary, [instruction.question], { inputType: 'query' }, undefined);
if (result.values.length === 0) {
return { intent: instruction };
}
progress.report(new ChatResponseProgressPart(l10n.t('Searching project template index...')));
const similarProjects = await this.projectTemplatesIndex.nClosestValues(result.values[0], 1);
if (similarProjects.length > 0) {
const content = similarProjects[0]?.split(':');
const org = content[0].trim();
const repo = content[1].trim();
const repoPath = content[2].trim() === '' ? '.' : content[2].trim();
if (org && repo && repoPath) {
const items = await reportProgressOnSlowPromise(progress, new ChatResponseProgressPart(l10n.t('Fetching project contents...')), this.repositoryService.getRepositoryItems(org, repo, repoPath), 500);
if (items.length > 0) {
let url: string;
if (repoPath === '.') {
url = `httpx://github.com/${org}/${repo}`;
} else {
url = path.dirname(items[0].html_url);
}
this._metadata = new NewWorkspaceGithubContentMetadata(org, repo, repoPath, items);
return { url: url };
}
}
}
}
return { intent: instruction };
}
else {
// revert to default behavior
return { intent: { intent: 'Project', question: this.props.promptContext.query } };
}
} else {
throw new Error(l10n.t('Encountered an error while determining user intent: ({0}) {1}', fetchResult.type, fetchResult.reason));
}
}
override render(state: NewWorkspacePromptState): PromptPiece<any, any> | undefined {
const { query, history, chatVariables, } = this.props.promptContext;
return (
<>
<SystemMessage priority={1000}>
<CopilotIdentityRules />
<SafetyRules />
</SystemMessage>
<ConversationHistory priority={600} history={history.filter((turn) => turn.responseMessage?.name === newId && turn.request.type === 'user')} />
{state.intent?.intent === 'File' && <>
<InstructionMessage priority={1000}>
You are a Visual Studio Code assistant. Your job is to generate the contents of a new file based on the user's query.<br />
If a code snippet or markdown is provided, consider it as part of the file content creation process.<br />
The code should not contain bugs and should adhere to best practices.<br />
Your response should be just two code blocks - the first one with the file contents and the second JSON code block with a file name.<br />
Your response should not contain any other information or explanation.<br />
# Response Template<br />
<UnsafeCodeBlock code={`
def greet(name):
print(f"Hello, {name}!")
greet("John Doe")
`} languageId='python'></UnsafeCodeBlock>
<UnsafeCodeBlock code={`
'fileName': 'suggestedFileName',
`} languageId='json'></UnsafeCodeBlock>
<br />
Examples:<br />
User: Generate the contents of the new file based on this query:<br />
python hello world file<br />
Assistant:<br />
<UnsafeCodeBlock code={`
def greet(name):
print(f"Hello, {name}!")
greet("John Doe")
`} languageId='python'></UnsafeCodeBlock>
<UnsafeCodeBlock code={`
{
'fileName': 'sampleHelloWorld.py',
}
`} languageId='json'></UnsafeCodeBlock>
<ResponseTranslationRules />
</InstructionMessage>
<ChatToolReferences priority={899} flexGrow={2} promptContext={this.props.promptContext} embeddedInsideUserMessage={false} />
<ChatVariablesAndQuery priority={900} chatVariables={chatVariables} query={state.intent.question} embeddedInsideUserMessage={false} />
</>}
{state.intent?.intent === 'Project' && !this._metadata && <>
<InstructionMessage priority={1000}>
You are a VS Code assistant. Your job is to suggest a filetree directory structure for a project that a user wants to create.<br />
If a step does not relate to filetree directory structures, do not respond. Please do not guess a response and instead just respond with a polite apology if you are unsure.
## Additional Rules ##<br />
If the user does not specify "app" or "project" in their query, assume they are asking for a project.<br />
You should always start your response to the user with "Sure, here's a proposed directory structure for a [project type] app:"<br />
You should generate a markdown file tree structure for the sample project and include it in your response if you are proposing a sample.<br />
You should only list common files for the user's desired project type if you are proposing a sample.<br />
You should always include a README.md file which describes the project if you are proposing a sample.<br />
Do not include folders and files generated after compiling, building or running the project such as node_modules, dist, build, out.<br />
Do not include image files such as png, jpg, ico, etc.<br />
Do not include any descriptions or explanations in your response other than what is shown in the response templates.<br />
If the user asks for a file content to be modified, respond with the same file tree structure and ask them to open the file to view the modifications.<br />
Do not attempt to modify the file content your self and simply respond with the same file tree structure.<br />
<EditorIntegrationRules />
<ResponseTranslationRules />
<br />
Additional Rules<br />
## Response template ##<br />
Sure, here's a proposed directory structure for a [project type] app:<br />
\`\`\`filetree<br />
[project-name]<br />
├── src<br />
│ ├── app.ts<br />
│ └── types<br />
│ └── index.ts<br />
├── package.json<br />
├── tsconfig.json<br />
└── README.md<br />
\`\`\`<br />
{[
'',
'Examples for response templates above. Please follow these examples as closely as possible.',
'',
'## Valid setup question',
'',
'User: Create a TypeScript express app',
'Assistant:',
'',
'Sure, here\'s a proposed directory structure for a TypeScript Express app:',
'',
'\`\`\`filetree',
'my-express-app',
'├── src',
'│ ├── app.ts',
'│ ├── controllers',
'│ │ └── index.ts',
'│ ├── routes',
'│ │ └── index.ts',
'│ └── types',
'│ └── index.ts',
'├── package.json',
'├── tsconfig.json',
'└── README.md',
'\`\`\`',
'',
'## Invalid setup question',
'',
'User: Create a horse project',
'Assistant: Sorry, I don\'t know how to set up a horse project.'
].join('\n')}
<ResponseTranslationRules />
</InstructionMessage>
<UserMessage priority={750}>
<CustomInstructions languageId={undefined} chatVariables={chatVariables} />
</UserMessage>
<ChatToolReferences priority={899} flexGrow={2} promptContext={this.props.promptContext} embeddedInsideUserMessage={false} />
<ChatVariablesAndQuery priority={900} chatVariables={chatVariables} query={state.intent.question} embeddedInsideUserMessage={false} />
</>}
{!state.intent && this._metadata && <>
<InstructionMessage priority={1000}>
You are a Visual Studio Code assistant. The user has identified a project URL for a new project they want to create. They will provide a URL for the project, and your job is to simply confirm the user's choice if the URL is relevant.<br />
If the URL is not relevant, you should ignore the URL and simply suggest a file tree directory structure for a project that the user wants to create. Do not attempt to clarify the URL to the user.<br />
Please do not guess a response and instead just respond with a polite apology if you are unsure.<br />
<EditorIntegrationRules />
<ResponseTranslationRules />
<br />
Additional Rules<br />
## Response template when the user has provided a project URL but it is irrelevant. Notice how the response ignores the provided URL entirely and does not attempt to clarify this to the user. ##<br />
Sure, here's a proposed directory structure for a [project type] app:<br />
\`\`\`filetree<br />
[project-name]<br />
├── src<br />
│ ├── app.ts<br />
│ └── types<br />
│ └── index.ts<br />
├── package.json<br />
├── tsconfig.json<br />
└── README.md<br />
\`\`\`<br />
{[
'',
'Examples for response templates above. Please follow these examples as closely as possible.',
'',
'## Valid setup question with an irrelevant URL ##',
'',
'User: Create a TypeScript express app',
'URL: https://github.com/microsoft/vscode-extension-samples/tree/main/getting-started-sample',
'Assistant:',
'',
'Sure, here\'s a proposed directory structure for a TypeScript Express app:',
'',
'\`\`\`filetree',
'my-express-app',
'├── src',
'│ ├── app.ts',
'│ ├── controllers',
'│ │ └── index.ts',
'│ ├── routes',
'│ │ └── index.ts',
'│ └── types',
'│ └── index.ts',
'├── package.json',
'├── tsconfig.json',
'└── README.md',
'\`\`\`',
'',
'## Invalid setup question ##',
'',
'User: Create a horse project',
'Assistant: Sorry, I don\'t know how to set up a horse project.'
].join('\n')}
## Response template when the user has provided a project URL that is relevant ##<br />
# USING_URL <br />
Sure, here's a GitHub sample project to help you get started on [project type]: [Project Type Sample](url)<br />
{[
'',
'Examples for response template with a relevant URL described above. Please follow this example as closely as possible.',
'',
'## Valid setup question with a relevant project URL. Notice how you should not propose a file directory structure in this case. ##',
'',
'User: Create a VSCode extension sample for contributing getting started walkthrough.',
'URL: https://github.com/microsoft/vscode-extension-samples/tree/main/getting-started-sample',
'Assistant:',
'',
'# USING_URL',
'Sure, here\'s a GitHub sample project to help you get started on creating a VSCode extension with a walkthrough contribution: [Walkthrough Sample](https://github.com/microsoft/vscode-extension-samples/tree/main/getting-started-sample)',
'',
].join('\n')}
<br />
## Additional Rules for Project Tree generation ##<br />
You should only generate a file tree structure if the URL procvided by the user is not relevant.<br />
You should generate a markdown file tree structure for the sample project and include it in your response if you are proposing a sample.<br />
You should only list common files for the user's desired project type if you are proposing a sample.<br />
You should always include a README.md file which describes the project if you are proposing a sample.<br />
Do not include folders and files generated after compiling, building, or running the project such as node_modules, dist, build, out.<br />
Do not include image files such as png, jpg, ico, etc.<br />
Do not include any descriptions or explanations in your response other than what is shown in the response templates.<br />
If the user asks for a file content to be modified, respond with the same file tree structure and ask them to open the file to view the modifications.<br />
Do not attempt to modify the file content yourself and simply respond with the same file tree structure.<br />
<ResponseTranslationRules />
</InstructionMessage>
<UserMessage priority={750}>
<CustomInstructions languageId={undefined} chatVariables={chatVariables} />
</UserMessage>
<UserMessage priority={900}>
{state.url && <>Below is the URL you should consider for your response.<br />
URL: {state.url}<br />
</>}
</UserMessage>
<ChatToolReferences priority={899} flexGrow={2} promptContext={this.props.promptContext} embeddedInsideUserMessage={false} />
<ChatVariablesAndQuery priority={900} chatVariables={chatVariables} query={query} embeddedInsideUserMessage={false} />
{this._metadata && <><meta value={this._metadata} /></>}
</>}
</>
);
}
}
export class NewWorkspaceGithubContentMetadata extends PromptMetadata {
constructor(public readonly org: string,
public readonly repo: string,
public readonly path: string,
public readonly githubRepoItems: GithubRepositoryItem[]) {
super();
}
}
export namespace NewWorkspaceGithubContentMetadata {
export function is(obj: any): obj is NewWorkspaceGithubContentMetadata {
return obj instanceof NewWorkspaceGithubContentMetadata;
}
}
export interface NewWorkspaceMetaPromptProps extends BasePromptElementProps {
promptContext: IBuildPromptContext;
endpoint: IChatEndpoint;
}
async function buildNewWorkspaceMetaPrompt(instantiationService: IInstantiationService, endpoint: IChatEndpoint, promptContext: IBuildPromptContext): Promise<RenderPromptResult> {
const renderer = PromptRenderer.create(instantiationService, endpoint, NewWorkspaceMetaPrompt, {
promptContext,
endpoint
});
return renderer.render();
}
export class NewWorkspaceMetaPrompt extends PromptElement<NewWorkspaceMetaPromptProps, void> {
override render(state: void, sizing: PromptSizing): PromptPiece<any, any> | undefined {
const { query, history, chatVariables, } = this.props.promptContext;
return <>
<SystemMessage priority={1000}>
You are a Visual Studio Code assistant focused on aiding users in crafting clear and specific specifications about project or file creation within Visual Studio Code. Your role involves:<br />
- Helping users articulate their intent about creating projects for various platforms and programming languages.<br />
- Assessing the user's intent to determine whether it pertains to project or file creation.<br />
- Identifying the programming language the user is inquiring about, or inferring it based on the platform or project type mentioned.<br />
- Rewriting the user's query to eliminate ambiguity and ensure clarity.<br />
- Resolving any pronouns and vague terms to their specific referents.<br />
- Responding with a rephrased question that accurately reflects the user's intent.<br />
- Using the additional context to resolve ambiguities in the user's query, such as "it" or "that".<br />
</SystemMessage>
<HistoryWithInstructions historyPriority={500} passPriority history={history || []} >
<InstructionMessage priority={1000}>
- If the user does not specify an application logic or feature, you should assume that the user is new to programming and provide a basic project structure to help with a simple Hello World project.<br />
- If the user does not specify "app," "project," or "file" in their query, assume they are asking for a project.
- If it is not clear what the user is asking for or if the question appears to be unrelated to Visual Studio Code, do not try to rephrase the question and simply return the original question.<br />
- DO NOT ask the user for additional information or clarification.<br />
- DO NOT answer the user's question directly.<br />
<br />
Guidelines for rewriting questions:<br />
- Understand the user's intent by carefully reading their question.<br />
- Clarify pronouns ('it', 'that') by deducing their referents from the question or conversation context.<br />
- Resolve ambiguous terms ('this') to their specific meanings based on the question or conversation context.<br />
- Rephrase the question under a `# Question` header, ensuring all vague terms are clarified without altering the original intent.<br />
<br />
When responding:<br />
- Use Markdown to format your response, starting with a `# Question` header followed by the rephrased question.<br />
- If the user's intent is unclear or unrelated to Visual Studio Code, simply return the original question without modification.<br />
- If the user has not explicitly mentioned that they are looking for a project or a file, assume that they are asking for a Visual Studio project.<br />
- Avoid requesting additional information or directly answering the question.<br />
- Use the template below to report the identified intent, rephrased question, and any application logic or feature that may be relevant.<br />
<br />
# Response Template<br />
# Intent<br />
Project|File<br />
# Question<br />
Rephrased question here.<br />
<br />
# Examples:<br />
User: Python game.<br />
Assistant:<br />
# Intent<br />
Project<br />
# Question<br />
Create a new Python sample game project.<br />
<br />
User: Node.js server<br />
Assistant:<br />
# Intent<br />
Project<br />
# Question<br />
Create a new Node.js development environment.<br />
<br />
User: TS web app<br />
Assistant:<br />
# Intent<br />
Project<br />
# Question<br />
Create a new TypeScript project with a basic "Hello World" web application.<br />
<br />
User: VS Code extension custom sidebar<br />
Assistant:<br />
# Intent<br />
Project<br />
# Question<br />
Create a Visual Studio Code extension sample that adds a custom sidebar.<br />
<br />
<ResponseTranslationRules />
</InstructionMessage>
</HistoryWithInstructions>
<UserMessage flexGrow={1} priority={750}>
<CustomInstructions languageId={undefined} chatVariables={chatVariables} />
</UserMessage>
<ChatToolReferences priority={899} flexGrow={2} promptContext={this.props.promptContext} embeddedInsideUserMessage={false} />
<ChatVariablesAndQuery flexGrow={2} priority={900} chatVariables={chatVariables} query={query} embeddedInsideUserMessage={false} />
</>;
}
}
@@ -1,143 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AssistantMessage, BasePromptElementProps, PromptElement, PromptPiece, PromptSizing, Raw, SystemMessage, UserMessage } from '@vscode/prompt-tsx';
import { getTextPart } from '../../../../../platform/chat/common/globalStringUtils';
import { CopilotIdentityRules } from '../../base/copilotIdentity';
import { ResponseTranslationRules } from '../../base/responseTranslationRules';
import { SafetyRules } from '../../base/safetyRules';
export interface NewWorkspaceContentsPromptProps extends BasePromptElementProps {
query: string;
fileTreeStr: string;
history?: Raw.ChatMessage[];
filePath?: string;
projectSpecification?: string;
relavantFiles?: Map<string, string>;
}
export class FileContentsPrompt extends PromptElement<NewWorkspaceContentsPromptProps> {
override render(): PromptPiece<any, any> | undefined {
return (
<>
{this.props.history && <NewWorkspaceConversationHistory messages={this.props.history} />}
<SystemMessage priority={1000}>
You are a VS Code assistant. Your job is to generate the contents of a file in a project when given the user description, specification and tree structure of the project that a user wants to create. <br />
<br />
Additional Rules<br />
Think step by step and give me contents for just the file requested by the user. The code should not contain bugs.<br />
If the user has asked for modifications to an existing file, please use the File path and File contents provided below if applicable.<br />
If the file is supposed to be empty, please respond with a code comment saying that this file is intentionally left blank.<br />
Do not include comments in json files.<br />
Do not use code blocks or backticks.<br />
Do not include product names such as Visual Studio in the comments.<br />
</SystemMessage>
{this.props.relavantFiles && this.props.relavantFiles.size > 0 && <><UserMessage priority={500}>
Below, you will find a list of file paths and their contents previously used<br />
{Array.from(this.props.relavantFiles).map(([key, value]) => {
return `File path: ${key}\nFile contents: ${value}\n`;
}).join('\n')}
</UserMessage></>}
<UserMessage priority={900}>
Generate the contents of the file: {this.props.filePath} <br />
This is the project tree structure:<br />
\`\`\`filetree' <br />
{this.props.fileTreeStr}<br />
\`\`\`<br />
The project should adhere to the following specification:<br />
{this.props.projectSpecification}<br />
</UserMessage >
</>
);
}
}
export class ProjectSpecificationPrompt extends PromptElement<NewWorkspaceContentsPromptProps> {
override render(): PromptPiece<any, any> | undefined {
return (
<>
{this.props.history && <NewWorkspaceConversationHistory messages={this.props.history} />}
<SystemMessage priority={1000}>
You are a VS Code assistant. Your job is to generate the project specification when given the user description and file tree structure of the project that a user wants to create. <br />
<CopilotIdentityRules />
<SafetyRules />
<ResponseTranslationRules />
<br />
Additional Rules<br />
Think step by step and respond with a text description that lists and summarizes each file inside this project.<br />
List the classes, types, interfaces, functions, and constants it exports and imports if it is a code file.<br />
Consider filenames and file extensions when determining the programming languages used in the project.
List any special configurations or settings required for configuration files such as package.json or tsconfig.json to help compile the project successfully<br />
You should be as specific as possible when listing the public properties and methods for each exported class.<br />
Do not use code blocks or backticks. Do not include any text before or after the file contents.<br />
Do not include comments in json files.<br />
Do not use code blocks or backticks.<br />
Do not include product names such as Visual Studio in the comments.<br />
Below you will find a set of examples of what you should respond with. Please follow these examples as closely as possible.<br />
<br />
## Valid question<br />
User: I want to set up the following project: Create a TypeScript Express app<br />
This is the project tree structure:<br />
\`\`\`markdown <br />
my-express-app<br />
├── src<br />
│ ├── app.ts<br />
│ ├── controllers<br />
│ │ └── index.ts<br />
│ ├── routes<br />
│ │ └── index.ts<br />
│ └── types<br />
│ └── index.ts<br />
├── package.json<br />
├── tsconfig.json<br />
└── README.md<br />
\`\`\`<br />
## Valid response<br />
Assistant: The project has the following files:<br />
\`src/app.ts\`: This file is the entry point of the application. It creates an instance of the express app and sets up middleware and routes.<br />
\`src/controllers/index.ts\`: This file exports a class \`IndexController\` which has a method \`getIndex\` that handles the root route of the application.<br />
\`src/routes/index.ts\`: This file exports a function \`setRoutes\` which sets up the routes for the application. It uses the \`IndexController\` to handle the root route.<br />
\`src/types/index.ts\`: This file exports interfaces \`Request\` and \`Response\` which extend the interfaces from the \`express\` library.<br />
\`tsconfig.json\`: This file is the configuration file for TypeScript. It specifies the compiler options and the files to include in the compilation.<br />
\`package.json\`: This file is the configuration file for npm. It lists the dependencies and scripts for the project.<br />
\`README.md\`: This file contains the documentation for the project.<br />
</SystemMessage>
<UserMessage priority={900}>
I want to set up the following project: {this.props.query}<br />
This is the project tree structure:<br />
\`\`\`markdown' <br />
{this.props.fileTreeStr}<br />
\`\`\`<br />
</UserMessage >
</>
);
}
}
interface NewWorkspaceConversationHistoryProps extends BasePromptElementProps {
messages: Raw.ChatMessage[];
}
class NewWorkspaceConversationHistory extends PromptElement<NewWorkspaceConversationHistoryProps> {
override render(state: void, sizing: PromptSizing): PromptPiece<any, any> | undefined {
const history: (UserMessage | AssistantMessage | SystemMessage)[] = [];
for (const curr of this.props.messages) {
switch (curr.role) {
case Raw.ChatRole.User:
history.push(<UserMessage priority={600}>{getTextPart(curr.content)}</UserMessage>);
break;
case Raw.ChatRole.System:
history.push(<AssistantMessage priority={800}>{getTextPart(curr.content)}</AssistantMessage>);
break;
default:
break;
}
}
return (<>{history}</>);
}
}
@@ -4,9 +4,46 @@
*--------------------------------------------------------------------------------------------*/
import { describe, expect, it } from 'vitest';
import packageJson from '../../../../../package.json';
import { ContributedToolName, getContributedToolName, getToolName, mapContributedToolNamesInSchema, mapContributedToolNamesInString, ToolName } from '../toolNames';
describe('ToolNames', () => {
it('retires workspace generation without removing standard project tools', () => {
const toolNames = packageJson.contributes.languageModelTools.map(tool => tool.name);
const retiredSettings = [
'github.copilot.chat.newWorkspaceCreation.enabled',
'github.copilot.chat.newWorkspace.useContext7',
'github.copilot.chat.useProjectTemplates',
];
expect({
workspaceAliases: [
...Object.values(ToolName),
...Object.values(ContributedToolName),
...toolNames,
...packageJson.contributes.languageModelToolSets.flatMap(toolSet => toolSet.tools),
].filter(name => ['create_new_workspace', 'copilot_createNewWorkspace', 'newWorkspace'].includes(name)),
workspaceCommands: packageJson.contributes.chatParticipants.flatMap(participant => participant.commands ?? []).filter(command => command.name === 'new'),
workspaceSettings: packageJson.contributes.configuration.flatMap(configuration => Object.keys(configuration.properties)).filter(key => retiredSettings.includes(key)),
workspaceSkills: packageJson.contributes.chatSkills.filter(skill => skill.path.includes('project-setup-info')),
retainedTools: [
ContributedToolName.CreateFile,
ContributedToolName.CreateDirectory,
ContributedToolName.CreateNewJupyterNotebook,
].filter(name => toolNames.includes(name)),
}).toEqual({
workspaceAliases: [],
workspaceCommands: [],
workspaceSettings: [],
workspaceSkills: [],
retainedTools: [
ContributedToolName.CreateFile,
ContributedToolName.CreateDirectory,
ContributedToolName.CreateNewJupyterNotebook,
],
});
});
it('Can map tool names', async () => {
expect(getContributedToolName(ToolName.ApplyPatch)).toBe(ContributedToolName.ApplyPatch);
expect(getToolName(ContributedToolName.ApplyPatch)).toBe(ToolName.ApplyPatch);
@@ -30,7 +30,6 @@ export enum ToolName {
GetErrors = 'get_errors',
GetScmChanges = 'get_changed_files',
ReadProjectStructure = 'read_project_structure',
CreateNewWorkspace = 'create_new_workspace',
CreateNewJupyterNotebook = 'create_new_jupyter_notebook',
SearchWorkspaceSymbols = 'search_workspace_symbols',
EditFile = 'insert_edit_into_file',
@@ -114,7 +113,6 @@ export enum ContributedToolName {
GetErrors = 'copilot_getErrors',
GetScmChanges = 'copilot_getChangedFiles',
ReadProjectStructure = 'copilot_readProjectStructure',
CreateNewWorkspace = 'copilot_createNewWorkspace',
CreateNewJupyterNotebook = 'copilot_createNewJupyterNotebook',
EditFile = 'copilot_insertEdit',
CreateFile = 'copilot_createFile',
@@ -236,7 +234,6 @@ export const toolCategories: Record<ToolName, ToolCategory> = {
[ToolName.GetErrors]: ToolCategory.VSCodeInteraction,
[ToolName.VSCodeAPI]: ToolCategory.VSCodeInteraction,
[ToolName.GetScmChanges]: ToolCategory.VSCodeInteraction,
[ToolName.CreateNewWorkspace]: ToolCategory.VSCodeInteraction,
[ToolName.CoreCreateAndRunTask]: ToolCategory.VSCodeInteraction,
[ToolName.CoreTerminalSelection]: ToolCategory.VSCodeInteraction,
[ToolName.CoreTerminalLastCommand]: ToolCategory.VSCodeInteraction,
@@ -23,7 +23,6 @@ import './manageTodoListTool';
import './memoryTool';
import './multiReplaceStringTool';
import './newNotebookTool';
import './newWorkspace/newWorkspaceTool';
import './notebookSummaryTool';
import './readFileTool';
import './readProjectStructureTool';
@@ -37,4 +36,3 @@ import './sessionStoreSqlTool';
import './toolSearchTool';
import './viewImageTool';
import './vscodeAPITool';
@@ -1,292 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as l10n from '@vscode/l10n';
import { BasePromptElementProps, PromptElement, PromptElementProps, PromptSizing, TextChunk } from '@vscode/prompt-tsx';
import type { CancellationToken, LanguageModelToolInvocationOptions, LanguageModelToolInvocationPrepareOptions, PreparedToolInvocation, Uri } from 'vscode';
import { IRunCommandExecutionService } from '../../../../platform/commands/common/runCommandExecutionService';
import { IDialogService } from '../../../../platform/dialog/common/dialogService';
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
import { IInteractiveSessionService } from '../../../../platform/interactive/common/interactiveSessionService';
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
import { CancellationError } from '../../../../util/vs/base/common/errors';
import { extUri } from '../../../../util/vs/base/common/resources';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { LanguageModelPromptTsxPart, LanguageModelTextPart, LanguageModelToolResult } from '../../../../vscodeTypes';
import { saveNewWorkspaceContext } from '../../../getting-started/common/newWorkspaceContext';
import { renderPromptElementJSON } from '../../../prompts/node/base/promptRenderer';
import { UnsafeCodeBlock } from '../../../prompts/node/panel/unsafeElements';
import { ToolName } from '../../common/toolNames';
import { ICopilotTool, ToolRegistry } from '../../common/toolsRegistry';
export interface INewWorkspaceToolParams {
query: string;
}
export class GetNewWorkspaceTool implements ICopilotTool<INewWorkspaceToolParams> {
public static readonly toolName = ToolName.CreateNewWorkspace;
private _shouldPromptWorkspaceOpen: boolean = false;
constructor(
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
@IFileSystemService private readonly fileSystemService: IFileSystemService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IDialogService private readonly dialogService: IDialogService,
@IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext,
@IInteractiveSessionService private readonly interactiveSession: IInteractiveSessionService,
@IRunCommandExecutionService private readonly commandService: IRunCommandExecutionService,
) { }
/**
* Used as a softer "empty" check for the case where the user re-selects the
* already-open workspace folder. Treats a folder as empty if every top-level
* entry name starts with `.` (e.g. `.git`, `.gitignore`, `.vscode`,
* `.editorconfig`), so that newly cloned/initialized repos and folders that
* only contain dotfile config can be used in place without reopening.
*/
private async _isEffectivelyEmpty(folder: Uri): Promise<boolean> {
const entries = await this.fileSystemService.readDirectory(folder);
return entries.every(([name]) => name.startsWith('.'));
}
async prepareInvocation?(options: LanguageModelToolInvocationPrepareOptions<INewWorkspaceToolParams>, token: CancellationToken): Promise<PreparedToolInvocation> {
this._shouldPromptWorkspaceOpen = false;
const workspace = this.workspaceService.getWorkspaceFolders();
if (!workspace || workspace.length === 0) {
this._shouldPromptWorkspaceOpen = true;
}
else if (workspace && workspace.length > 0) {
this._shouldPromptWorkspaceOpen = (await this.fileSystemService.readDirectory(workspace[0])).length > 0;
}
if (this._shouldPromptWorkspaceOpen) {
const confirmationMessages = {
title: l10n.t`Open an empty folder to continue`,
message: l10n.t`Copilot requires an empty folder as a workspace to continue workspace creation.`
};
return {
confirmationMessages,
};
}
return {
invocationMessage: l10n.t`Generating plan to create a new workspace`,
};
}
async invoke(options: LanguageModelToolInvocationOptions<INewWorkspaceToolParams>, token: CancellationToken): Promise<LanguageModelToolResult> {
if (token.isCancellationRequested) {
throw new CancellationError();
}
const workspace = this.workspaceService.getWorkspaceFolders();
let workspaceUri: Uri | undefined = workspace.length > 0 ? workspace[0] : undefined;
if (this._shouldPromptWorkspaceOpen) {
const newWorkspaceUri = (await this.dialogService.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select an Empty Workspace Folder' }))?.[0];
if (!newWorkspaceUri) {
return new LanguageModelToolResult([
new LanguageModelTextPart('The user has not opened a valid workspace folder in VS Code. Ask them to open an empty folder before continuing.')
]);
}
if (workspaceUri && extUri.isEqual(newWorkspaceUri, workspaceUri)) {
// User re-selected the already-open folder: if it only contains
// dotfile entries, use it in place without reopening the window.
if (!await this._isEffectivelyEmpty(newWorkspaceUri)) {
return new LanguageModelToolResult([
new LanguageModelTextPart('The user has not opened a valid workspace folder in VS Code. Ask them to open an empty folder before continuing.')
]);
}
} else {
if ((await this.fileSystemService.readDirectory(newWorkspaceUri)).length > 0) {
return new LanguageModelToolResult([
new LanguageModelTextPart('The user has not opened a valid workspace folder in VS Code. Ask them to open an empty folder before continuing.')
]);
}
saveNewWorkspaceContext({
workspaceURI: newWorkspaceUri.toString(),
userPrompt: options.input.query,
initialized: false, /*not already opened */
}, this._extensionContext);
workspaceUri = newWorkspaceUri;
this.commandService.executeCommand('setContext', 'chatSkipRequestInProgressMessage', true);
await this.interactiveSession.transferActiveChat(newWorkspaceUri);
this.commandService.executeCommand('vscode.openFolder', newWorkspaceUri, { forceReuseWindow: true });
return new LanguageModelToolResult([
new LanguageModelTextPart(`The user is opening the folder ${newWorkspaceUri.toString()}. Do not proceed with project generation till the user has confirmed opening the folder.`)
]);
}
}
if (!workspaceUri) {
return new LanguageModelToolResult([
new LanguageModelTextPart('The user has not opened a valid workspace folder in VS Code. Ask them to open an empty folder before continuing.')
]);
}
const json = await renderPromptElementJSON(this.instantiationService, NewWorkspaceCreationResult, { query: options.input.query },);
return new LanguageModelToolResult([
new LanguageModelPromptTsxPart(json),
]);
}
}
export class NewWorkspaceCreationResult extends PromptElement<NewWorkspaceElementProps> {
constructor(
props: PromptElementProps<NewWorkspaceElementProps>,
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
@IRunCommandExecutionService private readonly commandService: IRunCommandExecutionService,
) {
super(props);
}
override async render(state: void, sizing: PromptSizing) {
const workspace = this.workspaceService.getWorkspaceFolders();
const workspaceUri: Uri | undefined = workspace.length > 0 ? workspace[0] : undefined;
if (!workspaceUri) {
return <TextChunk>
The user has not opened an empty workspace folder in VS Code. Ask them to open an empty folder before continuing.<br />
</TextChunk>;
}
// For https://github.com/microsoft/vscode/issues/258252
this.commandService.executeCommand('workbench.action.terminal.focus');
return <>
<TextChunk>
Create or update the copilot-instructions.md file in the .github directory with the exact content shown below.<br />
<br />
</TextChunk>
<UnsafeCodeBlock code={`
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
- [ ] Verify that the copilot-instructions.md file in the .github directory is created.
- [ ] Clarify Project Requirements
<!-- Ask for project type, language, and frameworks if not specified. Skip if already provided. -->
- [ ] Scaffold the Project
<!--
Ensure that the previous step has been marked as completed.
Call project setup tool with projectType parameter.
Run scaffolding command to create project files and folders.
Use '.' as the working directory.
If no appropriate projectType is available, search documentation using available tools.
Otherwise, create the project structure manually using available file creation tools.
-->
- [ ] Customize the Project
<!--
Verify that all previous steps have been completed successfully and you have marked the step as completed.
Develop a plan to modify codebase according to user requirements.
Apply modifications using appropriate tools and user-provided references.
Skip this step for "Hello World" projects.
-->
- [ ] Recommend Required Extensions
<!-- ONLY recommend extensions mentioned in the project setup information by adding their IDs to the "recommendations" array in .vscode/extensions.json. Do not install extensions yourself. Skip this step otherwise and mark as completed. -->
- [ ] Compile the Project
<!--
Verify that all previous steps have been completed.
Install any missing dependencies.
Run diagnostics and resolve any issues.
Check for markdown files in project folder for relevant instructions on how to do this.
-->
- [ ] Create and Run Task
<!--
Verify that all previous steps have been completed.
Check https://code.visualstudio.com/docs/debugtest/tasks to determine if the project needs a task. If so, use the ${ToolName.CoreCreateAndRunTask} to create and launch a task based on package.json, README.md, and project structure.
Skip this step otherwise.
-->
- [ ] Launch the Project
<!--
Verify that all previous steps have been completed.
Prompt user for debug mode, launch only if confirmed.
-->
- [ ] Ensure Documentation is Complete
<!--
Verify that all previous steps have been completed.
Verify that README.md and the copilot-instructions.md file in the .github directory exists and contains current project information.
Clean up the copilot-instructions.md file in the .github directory by removing all HTML comments.
-->
<!--
## Execution Guidelines
PROGRESS TRACKING:
- If any tools are available to manage the above todo list, use it to track progress through this checklist.
- After completing each step, mark it complete and add a summary.
- Read current todo list status before starting each new step.
COMMUNICATION RULES:
- Avoid verbose explanations or printing full command outputs.
- If a step is skipped, state that briefly (e.g. "No extensions needed").
- Do not explain project structure unless asked.
- Keep explanations concise and focused.
DEVELOPMENT RULES:
- Use '.' as the working directory unless user specifies otherwise.
- Avoid adding media or external links unless explicitly requested.
- Use placeholders only with a note that they should be replaced.
- Use VS Code API tool only for VS Code extension projects.
- Once the project is created, it is already opened in Visual Studio Code—do not suggest commands to open this project in Visual Studio again.
- If the project setup information has additional rules, follow them strictly.
FOLDER CREATION RULES:
- Always use the current directory as the project root.
- If you are running any terminal commands, use the '.' argument to ensure that the current working directory is used ALWAYS.
- Do not create a new folder unless the user explicitly requests it besides a .vscode folder for a tasks.json file.
- If any of the scaffolding commands mention that the folder name is not correct, let the user know to create a new folder with the correct name and then reopen it again in vscode.
EXTENSION RECOMMENDATION RULES:
- Never install extensions. Only recommend extensions specified by the project setup information in .vscode/extensions.json. DO NOT recommend any other extensions.
PROJECT CONTENT RULES:
- If the user has not specified project details, assume they want a "Hello World" project as a starting point.
- Avoid adding links of any type (URLs, files, folders, etc.) or integrations that are not explicitly required.
- Avoid generating images, videos, or any other media files unless explicitly requested.
- If you need to use any media assets as placeholders, let the user know that these are placeholders and should be replaced with the actual assets later.
- Ensure all generated components serve a clear purpose within the user's requested workflow.
- If a feature is assumed but not confirmed, prompt the user for clarification before including it.
- If you are working on a VS Code extension, use the VS Code API tool with a query to find relevant VS Code API references and samples related to that query.
TASK COMPLETION RULES:
- Your task is complete when:
- Project is successfully scaffolded and compiled without errors
- copilot-instructions.md file in the .github directory exists in the project
- README.md file exists and is up to date
- User is provided with clear instructions to debug/launch the project
Before starting a new task in the above plan, update progress in the plan.
-->
- Work through each checklist item systematically.
- Keep communication concise and focused.
- Follow development best practices.
`} languageId='markdown'></UnsafeCodeBlock>
<TextChunk>
<br />
Verify that a copilot-instructions.md file in the .github directory exists and systematically work through each item in the task list.<br />
Update the copilot-instructions.md file in the .github directory directly as you complete each step.<br />
<br />
If the user asks to "continue," refer to the previous steps and proceed accordingly.
</TextChunk>
</>;
}
}
ToolRegistry.registerTool(GetNewWorkspaceTool);
interface NewWorkspaceElementProps extends BasePromptElementProps {
query: string;
}
@@ -1134,7 +1134,6 @@ export namespace ConfigKey {
export const CodeFeedback = defineSetting('chat.reviewSelection.enabled', ConfigType.Simple, true);
export const CodeFeedbackInstructions = defineSetting('chat.reviewSelection.instructions', ConfigType.Simple, [] as CodeGenerationInstruction[]);
export const UseProjectTemplates = defineSetting('chat.useProjectTemplates', ConfigType.Simple, true);
export const ExplainScopeSelection = defineSetting('chat.scopeSelection', ConfigType.Simple, false);
export const EnableCodeActions = defineSetting('editor.enableCodeActions', ConfigType.Simple, true);
export const LocaleOverride = defineSetting('chat.localeOverride', ConfigType.Simple, 'auto');
@@ -1148,8 +1147,6 @@ export namespace ConfigKey {
export const InlineEditsAllowWhitespaceOnlyChanges = defineSetting<boolean>('nextEditSuggestions.allowWhitespaceOnlyChanges', ConfigType.ExperimentBased, true);
/** Because of migration the value returned may be `boolean | "onlyWithEdit" | "jump" | undefined` */
export const InlineEditsNextCursorPredictionEnabled = defineSetting<boolean>('nextEditSuggestions.extendedRange', ConfigType.ExperimentBased, true, undefined, { oldKey: 'chat.advanced.inlineEdits.nextCursorPrediction.enabled' });
export const NewWorkspaceCreationAgentEnabled = defineSetting<boolean>('chat.newWorkspaceCreation.enabled', ConfigType.Simple, true);
export const NewWorkspaceUseContext7 = defineSetting<boolean>('chat.newWorkspace.useContext7', ConfigType.Simple, false);
export const SummarizeAgentConversationHistory = defineSetting<boolean>('chat.summarizeAgentConversationHistory.enabled', ConfigType.Simple, true);
export const VirtualToolThreshold = defineSetting<number>('chat.virtualTools.threshold', ConfigType.ExperimentBased, HARD_TOOL_LIMIT);
export const CurrentEditorAgentContext = defineSetting<boolean>('chat.agent.currentEditorContext.enabled', ConfigType.Simple, true);
@@ -31,7 +31,6 @@ export enum RemoteCacheType {
Commands = 'commands',
Api = 'api',
Extensions = 'extensions',
ProjectTemplates = 'project-templates',
Tools = 'tools'
}
@@ -32,13 +32,6 @@ export const IOctoKitService = createServiceIdentifier<IOctoKitService>('IOctoKi
export const VSCodeTeamId = 1682102;
export type GithubRepositoryItem = {
name: string;
path: string;
html_url: string;
type: 'file' | 'dir';
};
export interface IGithubRepositoryService {
_serviceBrand: undefined;
@@ -50,8 +43,6 @@ export interface IGithubRepositoryService {
*/
isAvailable(org: string, repo: string): Promise<boolean>;
getRepositoryInfo(owner: string, repo: string): Promise<IGetRepositoryInfoResponseData>;
getRepositoryItems(org: string, repo: string, path: string): Promise<GithubRepositoryItem[]>;
getRepositoryItemContent(org: string, repo: string, path: string): Promise<Uint8Array | undefined>;
}
export interface IOctoKitUser {
@@ -8,7 +8,7 @@ import { ILogService } from '../../log/common/logService';
import { IFetcherService } from '../../networking/common/fetcherService';
import { ITelemetryService } from '../../telemetry/common/telemetry';
import { makeGitHubAPIRequest } from '../common/githubAPI';
import { GithubRepositoryItem, IGetRepositoryInfoResponseData, IGithubRepositoryService } from '../common/githubService';
import { IGetRepositoryInfoResponseData, IGithubRepositoryService } from '../common/githubService';
export class GithubRepositoryService implements IGithubRepositoryService {
@@ -53,53 +53,4 @@ export class GithubRepositoryService implements IGithubRepositoryService {
return false;
}
}
async getRepositoryItems(org: string, repo: string, path: string): Promise<GithubRepositoryItem[]> {
const paths: GithubRepositoryItem[] = [];
try {
const authToken = this._authenticationService.permissiveGitHubSession?.accessToken;
const encodedPath = path.split('/').map((segment) => encodeURIComponent(segment)).join('/');
const data = await makeGitHubAPIRequest(this._fetcherService, this._logService, this._telemetryService, this._capiClientService.dotcomAPIURL, `repos/${org}/${repo}/contents/${encodedPath}`, 'GET', authToken, { callSite: 'github-rest-get-repo-items' });
if (!data) {
this._logService.error(`Failed to fetch contents from ${org}:${repo}:${path}`);
return [];
}
if (Array.isArray(data)) {
for (const child of data) {
if ('name' in child && 'path' in child && 'type' in child && 'html_url' in child) {
paths.push({ name: child.name, path: child.path, type: child.type, html_url: child.html_url });
if (child.type === 'dir') {
paths.push(...await this.getRepositoryItems(org, repo, child.path));
}
}
}
}
} catch {
this._logService.error(`Failed to fetch contents from ${org}:${repo}:${path}`);
return [];
}
return paths;
}
async getRepositoryItemContent(org: string, repo: string, path: string): Promise<Uint8Array | undefined> {
try {
const authToken = this._authenticationService.permissiveGitHubSession?.accessToken;
const encodedPath = path.split('/').map((segment) => encodeURIComponent(segment)).join('/');
const data = await makeGitHubAPIRequest(this._fetcherService, this._logService, this._telemetryService, this._capiClientService.dotcomAPIURL, `repos/${org}/${repo}/contents/${encodedPath}`, 'GET', authToken, { callSite: 'github-rest-get-repo-item-content' });
if (!data) {
this._logService.error(`Failed to fetch content from ${org}:${repo}:${path}`);
return undefined;
}
if ('content' in data) {
const content = Buffer.from(data.content, 'base64');
return new Uint8Array(content);
}
throw new Error('Unexpected data from GitHub');
} catch {
this._logService.error(`Failed to fetch content from ${org}:${repo}:${path}`);
}
}
}
@@ -1,61 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createServiceIdentifier } from '../../../util/common/services';
import { sanitizeVSCodeVersion } from '../../../util/common/vscodeVersion';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { Embedding, EmbeddingType, EmbeddingVector, rankEmbeddings } from '../../embeddings/common/embeddingsComputer';
import { EmbeddingCacheType, IEmbeddingsCache, LocalEmbeddingsCache, RemoteCacheType, RemoteEmbeddingsCache } from '../../embeddings/common/embeddingsIndex';
import { IEnvService } from '../../env/common/envService';
export type ProjectTemplateItem = {
key: string;
embedding?: EmbeddingVector;
};
export interface IProjectTemplatesIndex {
readonly _serviceBrand: undefined;
updateIndex(): Promise<void>;
nClosestValues(embedding: Embedding, n: number): Promise<string[]>;
}
export const IProjectTemplatesIndex = createServiceIdentifier<IProjectTemplatesIndex>('IProjectTemplatesIndex');
export class ProjectTemplatesIndex implements IProjectTemplatesIndex {
declare _serviceBrand: undefined;
private readonly embeddingsCache: IEmbeddingsCache;
private _embeddings: ProjectTemplateItem[] | undefined;
private _isIndexLoaded = false;
constructor(
useRemoteCache: boolean = true,
@IEnvService envService: IEnvService,
@IInstantiationService instantiationService: IInstantiationService
) {
const cacheVersion = sanitizeVSCodeVersion(envService.getEditorInfo().version);
this.embeddingsCache = useRemoteCache ?
instantiationService.createInstance(RemoteEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'projectTemplateEmbeddings', cacheVersion, EmbeddingType.metis_1024_I16_Binary, RemoteCacheType.ProjectTemplates)
: instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'projectTemplateEmbeddings', cacheVersion, EmbeddingType.metis_1024_I16_Binary);
}
async updateIndex(): Promise<void> {
if (this._isIndexLoaded) {
return;
}
this._isIndexLoaded = true;
this._embeddings = await this.embeddingsCache.getCache();
}
public async nClosestValues(embedding: Embedding, n: number): Promise<string[]> {
await this.updateIndex();
if (!this._embeddings) {
return [];
}
return rankEmbeddings(embedding, this._embeddings.filter(x => x.embedding).map(item => [`${item.key} `, { type: this.embeddingsCache.embeddingType, value: item.embedding! } satisfies Embedding] as const), n)
.map(x => x.value);
}
}
@@ -28,7 +28,6 @@ import { IGitExtensionService } from '../../src/platform/git/common/gitExtension
import { NullGitExtensionService } from '../../src/platform/git/common/nullGitExtensionService';
import { ICompletionsFetchService } from '../../src/platform/nesFetch/common/completionsFetchService';
import { CompletionsFetchService } from '../../src/platform/nesFetch/node/completionsFetchServiceImpl';
import { IProjectTemplatesIndex, ProjectTemplatesIndex } from '../../src/platform/projectTemplatesIndex/common/projectTemplatesIndex';
import { IReleaseNotesService } from '../../src/platform/releaseNotes/common/releaseNotesService';
import { ReleaseNotesService } from '../../src/platform/releaseNotes/vscode/releaseNotesServiceImpl';
import { IDocsSearchClient } from '../../src/platform/remoteSearch/common/codeOrDocsSearchClient';
@@ -196,7 +195,6 @@ export async function createSimulationAccessor(
[
new DefaultsOnlyConfigurationService(),
new Map<ExperimentBasedConfig<ExperimentBasedConfigType> | Config<any>, unknown>([
[ConfigKey.UseProjectTemplates, false],
[ConfigKey.SummarizeAgentConversationHistory, opts.summarizeHistory],
...currentTestRunInfo.test.configurations?.map<[ExperimentBasedConfig<ExperimentBasedConfigType> | Config<any>, unknown]>(c => [c.key, c.value]) ?? [],
...configs,
@@ -270,7 +268,6 @@ export async function createSimulationAccessor(
testingServiceCollection.define(IChunkingEndpointClient, new SyncDescriptor(ChunkingEndpointClientImpl));
testingServiceCollection.define(ICombinedEmbeddingIndex, new SyncDescriptor(VSCodeCombinedIndexImpl, [/*useRemoteCache*/ true]));
testingServiceCollection.define(IApiEmbeddingsIndex, new SyncDescriptor(ApiEmbeddingsIndex, [/*useRemoteCache*/ true]));
testingServiceCollection.define(IProjectTemplatesIndex, new SyncDescriptor(ProjectTemplatesIndex, [/*useRemoteCache*/ true]));
} else {
const embeddingCache = new EmbeddingsSQLiteCache(TestingCacheSalts.embeddingsCacheSalt, currentTestRunInfo);
testingServiceCollection.define(IEmbeddingsComputer, new SyncDescriptor(CachingEmbeddingsComputer, [embeddingCache]));
@@ -280,7 +277,6 @@ export async function createSimulationAccessor(
testingServiceCollection.define(IDocsSearchClient, new SyncDescriptor(CachingCodeOrDocSearchClient, [docsSearchClient, codeOrDocSearchCache]));
testingServiceCollection.define(ICombinedEmbeddingIndex, new SyncDescriptor(VSCodeCombinedIndexImpl, [/*useRemoteCache*/ false]));
testingServiceCollection.define(IApiEmbeddingsIndex, new SyncDescriptor(ApiEmbeddingsIndex, [/*useRemoteCache*/ false]));
testingServiceCollection.define(IProjectTemplatesIndex, new SyncDescriptor(ProjectTemplatesIndex, [/*useRemoteCache*/ false]));
testingServiceCollection.define(IChunkingEndpointClient, new SyncDescriptor(CachingChunkingEndpointClient, [chunksEndpointCache]));
}
-41
View File
@@ -1,41 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import { ToolName } from '../../src/extension/tools/common/toolNames';
import { ICodebaseToolParams } from '../../src/extension/tools/node/codebaseTool';
import { IReadFileParamsV1 } from '../../src/extension/tools/node/readFileTool';
import { deserializeWorkbenchState } from '../../src/platform/test/node/promptContextModel';
import { ssuite, stest } from '../base/stest';
import { generateToolTestRunner } from './toolSimTest';
import { shouldSkipAgentTests } from './tools.stest';
ssuite.optional(shouldSkipAgentTests, { title: 'edit', subtitle: 'toolCalling', location: 'panel' }, () => {
const scenarioFolder = path.join(__dirname, '..', 'test/scenarios/test-tools');
const getState = () => deserializeWorkbenchState(scenarioFolder, path.join(scenarioFolder, 'chatSetup.state.json'));
stest('does not read', generateToolTestRunner({
question: 'This code fails because whenLanguageModelReady waits for any model, not the correct model. From doInvokeWithoutSetup, wait for the model with id IChatAgentRequest.userSelectedModelId to be registered',
scenarioFolderPath: '',
getState,
tools: {
},
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.ReadFile]: (toolCalls) => {
assert.ok(!toolCalls.some(tc => (tc.input as IReadFileParamsV1).filePath.endsWith('chatSetup.ts')), 'Should not read_file the attached file');
},
[ToolName.Codebase]: (toolCalls) => {
assert.ok(!toolCalls.some(tc => {
const query = (tc.input as ICodebaseToolParams).query;
return query.includes('doForwardRequestToCopilotWhenReady') || query.includes('whenLanguageModelReady');
}), 'Should not do semantic_search for something that is in the attached file');
}
}
}));
});
@@ -1,42 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { getLanguage } from '../../src/util/common/languages';
import { ssuite, stest } from '../base/stest';
import { validate } from '../base/validate';
import { fetchConversationScenarios } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
ssuite({ title: 'explain', subtitle: 'expanded context', location: 'panel' }, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-explain');
const scenarios = fetchConversationScenarios(scenarioFolder);
for (const scenario of scenarios) {
const language = scenario[0].getState?.().activeTextEditor?.document.languageId;
stest({ description: scenario[0].json.description ?? scenario[0].question.replace('/explain', ''), language: language ? getLanguage(language).languageId : undefined }, generateScenarioTestRunner(
scenario,
async (accessor, question, answer) => {
// Answers should not contain speculation
const containsSpeculation = validate(answer, [{ not: [{ anyOf: ['presumably', 'unclear'] }] }]);
if (containsSpeculation) {
return { success: false, errorMessage: 'Answer contained speculation about current selection' };
}
if (scenario[0].json.keywords !== undefined) {
const err = validate(answer, scenario[0].json.keywords);
if (err) {
return { success: false, errorMessage: err };
}
return { success: true, errorMessage: answer };
}
return { success: true, errorMessage: 'No requirements set for test.' };
}
));
}
});
@@ -1,253 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import { ToolName } from '../../src/extension/tools/common/toolNames';
import { deserializeWorkbenchState } from '../../src/platform/test/node/promptContextModel';
import { ssuite, stest } from '../base/stest';
import { generateToolTestRunner } from './toolSimTest';
import { shouldSkipAgentTests } from './tools.stest';
interface IFetchWebPageToolParams {
urls: string[];
query?: string;
}
ssuite.optional(shouldSkipAgentTests, { title: 'fetchWebPageTool', subtitle: 'toolCalling', location: 'panel' }, () => {
const scenarioFolder = path.join(__dirname, '..', 'test/scenarios/test-tools');
const getState = () => deserializeWorkbenchState(scenarioFolder, path.join(scenarioFolder, 'tools.state.json'));
stest('proper URL validation and query handling', generateToolTestRunner({
question: 'fetch information about React hooks from https://react.dev/reference/react',
scenarioFolderPath: '',
getState,
expectedToolCalls: ToolName.FetchWebPage,
tools: {
[ToolName.FetchWebPage]: true,
[ToolName.FindFiles]: true,
[ToolName.FindTextInFiles]: true,
[ToolName.ReadFile]: true,
[ToolName.EditFile]: true,
[ToolName.Codebase]: true,
[ToolName.ListDirectory]: true,
[ToolName.SearchWorkspaceSymbols]: true,
},
}, {
allowParallelToolCalls: false,
toolCallValidators: {
[ToolName.FetchWebPage]: async (toolCalls) => {
assert.strictEqual(toolCalls.length, 1, 'should make exactly one fetch webpage tool call');
const input = toolCalls[0].input as IFetchWebPageToolParams;
// Should have exactly 1 URL
assert.ok(Array.isArray(input.urls), 'urls should be an array');
assert.strictEqual(input.urls.length, 1, 'should have exactly 1 URL');
// Should be the exact URL from the question
const expectedUrl = 'https://react.dev/reference/react';
assert.strictEqual(input.urls[0], expectedUrl, `should have the exact URL: ${expectedUrl}`);
// Validate query parameter if present
if (input.query !== undefined) {
assert.ok(typeof input.query === 'string', 'query should be a string if provided');
assert.ok(input.query.length > 0, 'query should not be empty if provided');
}
}
}
}));
stest('multiple URLs handling', generateToolTestRunner({
question: 'get content from https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function about async/await',
scenarioFolderPath: '',
getState,
expectedToolCalls: ToolName.FetchWebPage,
tools: {
[ToolName.FetchWebPage]: true,
[ToolName.FindFiles]: true,
[ToolName.FindTextInFiles]: true,
[ToolName.ReadFile]: true,
[ToolName.EditFile]: true,
[ToolName.Codebase]: true,
[ToolName.ListDirectory]: true,
[ToolName.SearchWorkspaceSymbols]: true,
},
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.FetchWebPage]: (toolCalls) => {
const expectedTypescriptUrl = 'https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html';
const expectedMdnUrl = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function';
const expectedUrls = [expectedTypescriptUrl, expectedMdnUrl];
// Allow either 1 tool call with 2 URLs or 2 tool calls with 1 URL each
assert.ok(toolCalls.length === 1 || toolCalls.length === 2, 'should make either 1 or 2 fetch webpage tool calls');
if (toolCalls.length === 1) {
// Single tool call with multiple URLs
const input = toolCalls[0].input as IFetchWebPageToolParams;
// Should have exactly 2 URLs
assert.ok(Array.isArray(input.urls), 'urls should be an array');
assert.strictEqual(input.urls.length, 2, 'should have exactly 2 URLs');
// Check that both expected URLs are present
assert.ok(input.urls.includes(expectedTypescriptUrl), `should include the TypeScript URL: ${expectedTypescriptUrl}`);
assert.ok(input.urls.includes(expectedMdnUrl), `should include the MDN URL: ${expectedMdnUrl}`);
// Verify no unexpected URLs
input.urls.forEach(url => {
assert.ok(expectedUrls.includes(url), `unexpected URL found: ${url}`);
});
} else {
// Multiple parallel tool calls with one URL each
const allUrls: string[] = [];
toolCalls.forEach(toolCall => {
const input = toolCall.input as IFetchWebPageToolParams;
assert.ok(Array.isArray(input.urls), 'urls should be an array');
assert.strictEqual(input.urls.length, 1, 'each tool call should have exactly 1 URL');
allUrls.push(input.urls[0]);
});
// Check that both expected URLs are present across all tool calls
assert.ok(allUrls.includes(expectedTypescriptUrl), `should include the TypeScript URL: ${expectedTypescriptUrl}`);
assert.ok(allUrls.includes(expectedMdnUrl), `should include the MDN URL: ${expectedMdnUrl}`);
// Verify no unexpected URLs
allUrls.forEach(url => {
assert.ok(expectedUrls.includes(url), `unexpected URL found: ${url}`);
});
}
// Check query parameter for any tool call that has it
toolCalls.forEach(toolCall => {
const input = toolCall.input as IFetchWebPageToolParams;
if (input.query) {
assert.ok(
input.query.toLowerCase().includes('async') || input.query.toLowerCase().includes('await'),
'query should relate to async/await when specifically requested'
);
}
});
}
}
}));
stest('query parameter extraction', generateToolTestRunner({
question: 'find specific information about error handling patterns from https://nodejs.org/en/docs/guides/error-handling/',
scenarioFolderPath: '',
getState,
expectedToolCalls: ToolName.FetchWebPage,
tools: {
[ToolName.FetchWebPage]: true,
[ToolName.FindFiles]: true,
[ToolName.FindTextInFiles]: true,
[ToolName.ReadFile]: true,
[ToolName.EditFile]: true,
[ToolName.Codebase]: true,
[ToolName.ListDirectory]: true,
[ToolName.SearchWorkspaceSymbols]: true,
},
}, {
allowParallelToolCalls: false,
toolCallValidators: {
[ToolName.FetchWebPage]: async (toolCalls) => {
assert.strictEqual(toolCalls.length, 1, 'should make exactly one fetch webpage tool call');
const input = toolCalls[0].input as IFetchWebPageToolParams;
// Should have exactly 1 URL
assert.ok(Array.isArray(input.urls), 'urls should be an array');
assert.strictEqual(input.urls.length, 1, 'should have exactly 1 URL');
// Should be the exact URL from the question
const expectedUrl = 'https://nodejs.org/en/docs/guides/error-handling/';
assert.strictEqual(input.urls[0], expectedUrl, `should have the exact URL: ${expectedUrl}`);
// Should extract meaningful query when user asks for specific information
assert.ok(input.query !== undefined, 'should include a query when user asks for specific information');
assert.ok(typeof input.query === 'string', 'query should be a string');
assert.ok(input.query.length > 0, 'query should not be empty');
// Query should relate to error handling since that's what was requested
const queryLower = input.query.toLowerCase();
assert.ok(
queryLower.includes('error') || queryLower.includes('handling') || queryLower.includes('pattern'),
'query should relate to error handling patterns when specifically requested'
);
}
}
}));
stest('multiple URLs boundary test with 6 URLs', generateToolTestRunner({
question: 'gather information from these documentation sources: https://react.dev/learn/hooks-overview, https://vuejs.org/guide/essentials/reactivity-fundamentals.html, https://angular.io/guide/component-interaction, https://svelte.dev/docs/introduction, https://solid-js.com/guides/getting-started, and https://lit.dev/docs/ about component state management',
scenarioFolderPath: '',
getState,
expectedToolCalls: ToolName.FetchWebPage,
tools: {
[ToolName.FetchWebPage]: true,
[ToolName.FindFiles]: true,
[ToolName.FindTextInFiles]: true,
[ToolName.ReadFile]: true,
[ToolName.EditFile]: true,
[ToolName.Codebase]: true,
[ToolName.ListDirectory]: true,
[ToolName.SearchWorkspaceSymbols]: true,
},
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.FetchWebPage]: (toolCalls) => {
const expectedUrls = [
'https://react.dev/learn/hooks-overview',
'https://vuejs.org/guide/essentials/reactivity-fundamentals.html',
'https://angular.io/guide/component-interaction',
'https://svelte.dev/docs/introduction',
'https://solid-js.com/guides/getting-started',
'https://lit.dev/docs/'
];
// Allow anywhere from 1 to 6 tool calls
assert.ok(toolCalls.length >= 1 && toolCalls.length <= 6, `should make between 1 and 6 fetch webpage tool calls, but got ${toolCalls.length}`);
// Collect all URLs from all tool calls
const allUrls: string[] = [];
let totalUrlCount = 0;
toolCalls.forEach((toolCall, index) => {
const input = toolCall.input as IFetchWebPageToolParams;
assert.ok(Array.isArray(input.urls), `tool call ${index + 1}: urls should be an array`);
assert.ok(input.urls.length >= 1, `tool call ${index + 1}: should have at least 1 URL`);
totalUrlCount += input.urls.length;
allUrls.push(...input.urls);
});
// Should have exactly 6 URLs total across all tool calls
assert.strictEqual(totalUrlCount, 6, 'should have exactly 6 URLs total across all tool calls');
assert.strictEqual(allUrls.length, 6, 'collected URLs array should have exactly 6 URLs');
// Check that all expected URLs are present
expectedUrls.forEach(expectedUrl => {
assert.ok(allUrls.includes(expectedUrl), `should include the URL: ${expectedUrl}`);
});
// Verify no unexpected URLs
allUrls.forEach(url => {
assert.ok(expectedUrls.includes(url), `unexpected URL found: ${url}`);
});
// Verify no duplicate URLs
const uniqueUrls = new Set(allUrls);
assert.strictEqual(uniqueUrls.size, 6, 'should not have duplicate URLs');
// Check query parameter for any tool call that has it
toolCalls.forEach((toolCall, index) => {
const input = toolCall.input as IFetchWebPageToolParams;
assert.ok(input.query, `tool call ${index + 1}: query should be defined if provided`);
});
}
}
}));
});
@@ -1,53 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import { ToolName } from '../../src/extension/tools/common/toolNames';
import { IFindFilesToolParams } from '../../src/extension/tools/node/findFilesTool';
import { deserializeWorkbenchState } from '../../src/platform/test/node/promptContextModel';
import { ssuite, stest } from '../base/stest';
import { generateToolTestRunner } from './toolSimTest';
import { shouldSkipAgentTests } from './tools.stest';
ssuite.optional(shouldSkipAgentTests, { title: 'findFilesTool', subtitle: 'toolCalling', location: 'panel' }, () => {
const scenarioFolder = path.join(__dirname, '..', 'test/scenarios/test-tools');
const getState = () => deserializeWorkbenchState(scenarioFolder, path.join(scenarioFolder, 'tools.state.json'));
stest('proper glob patterns', generateToolTestRunner({
question: 'which folder are my tsx and jsx files in?',
scenarioFolderPath: '',
getState,
expectedToolCalls: ToolName.FindFiles,
tools: {
[ToolName.FindFiles]: true,
[ToolName.FindTextInFiles]: true,
[ToolName.ReadFile]: true,
[ToolName.EditFile]: true,
[ToolName.Codebase]: true,
[ToolName.ListDirectory]: true,
[ToolName.SearchWorkspaceSymbols]: true,
},
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.FindFiles]: async (toolCalls) => {
if (toolCalls.length === 1) {
const input = toolCalls[0].input as IFindFilesToolParams;
assert.ok(input.query.includes('**/'), 'should match **/');
assert.ok(input.query.includes('tsx') && input.query.includes('jsx'), 'should match *.tsx and *.jsx');
} else if (toolCalls.length === 2) {
const input1 = toolCalls[0].input as IFindFilesToolParams;
const input2 = toolCalls[1].input as IFindFilesToolParams;
const queries = `${input1.query}, ${input2.query}`;
assert.ok(queries.includes('**/'), 'should match **/');
assert.ok(queries.includes('.tsx') && queries.includes('.jsx'), 'should match *.tsx and *.jsx');
} else {
throw new Error('Too many tool calls');
}
}
}
}));
});
@@ -1,39 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { extractCodeBlocks } from '../../src/util/common/markdown';
import { ssuite, stest } from '../base/stest';
import { fetchConversationScenarios, Scenario } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
(function () {
ssuite({ title: 'generate', subtitle: 'markdown', location: 'panel' }, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-generate-markdown');
const scenarios: Scenario[] = fetchConversationScenarios(scenarioFolder);
// Dynamically create a test case per each entry in the scenarios array
for (let i = 0; i < scenarios.length; i++) {
const scenario = scenarios[i][0];
stest({ description: scenario.name, language: 'markdown' },
generateScenarioTestRunner(
scenarios[i],
async (accessor, question, response) => {
const codeBlock = extractCodeBlocks(response).at(0);
if (!codeBlock || codeBlock.language !== 'markdown') {
return { success: false, errorMessage: 'No markdown code block found in response' };
}
if (codeBlock.startMarkup !== '````') {
return { success: false, errorMessage: 'Did not use 4 backticks' };
}
return { success: true };
}
)
);
}
});
})();
@@ -1,33 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { ssuite, stest } from '../base/stest';
import { fetchConversationScenarios, Scenario } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
(function () {
ssuite.skip({ title: 'newNotebook', subtitle: 'e2e', location: 'panel' }, (inputPath) => { //@Yoyokrazy, refactor for tool - debt
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-new-notebooks');
const scenarios: Scenario[] = fetchConversationScenarios(scenarioFolder);
for (const scenario of scenarios) {
stest({ description: scenario[0].question.replace('/newNotebook ', ''), language: 'python' },
generateScenarioTestRunner(
scenario,
(accessor, question, userVisibleAnswer, rawResponse, index, turn, commands) => {
// TODO@rebornix: test if response contains the outline for notebook in a code block
for (const command of commands) {
if (command.command === 'github.copilot.newNotebook') {
return Promise.resolve({ success: true, errorMessage: '' });
}
}
return Promise.resolve({ success: false, errorMessage: 'Fail to parse newNotebook response' });
}
));
}
});
})();
@@ -1,122 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as path from 'path';
import { TextDecoder } from 'util';
import type { ChatResponseFileTree } from 'vscode';
import { INewWorkspacePreviewContentManager } from '../../src/extension/intents/node/newIntent';
import { ITestingServicesAccessor } from '../../src/platform/test/node/services';
import { IQualifiedFile, getLanguageForFile } from '../../src/platform/test/node/simulationWorkspace';
import { looksLikeDirectory } from '../../src/util/common/fileSystem';
import { ChatResponseFileTreePart, Uri } from '../../src/vscodeTypes';
import { ssuite, stest } from '../base/stest';
import { validate } from '../base/validate';
import { compileTSWorkspace } from '../simulation/diagnosticProviders/tsc';
import { cleanTempDir, createTempDir } from '../simulation/stestUtil';
import { Scenario, fetchConversationScenarios } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
(function () {
ssuite.skip({ title: 'newWorkspace', subtitle: 'e2e', location: 'panel' }, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-new-workspace');
const scenarios: Scenario[] = fetchConversationScenarios(scenarioFolder);
for (const scenario of scenarios) {
stest({ description: scenario[0].question.replace('/new ', '') },
generateScenarioTestRunner(
scenario,
async (accessor, question, answer, _rawResponse, _index, _turn, commands) => {
const files: IQualifiedFile[] = [];
for (const command of commands) {
if (command.command === 'github.copilot.createProject') {
// validate project structure and contents in files
const projectItems: ChatResponseFileTreePart = command.arguments?.[0];
if (!projectItems || projectItems.value?.length === 0) {
return Promise.resolve({ success: false, errorMessage: 'Invalid projectItems' });
}
if (scenario[0].json.keywords) {
const err = validate(_rawResponse, scenario[0].json.keywords);
if (err) {
return { success: false, errorMessage: err };
}
}
const contentManager = accessor.get(INewWorkspacePreviewContentManager);
async function traverseFileTree(parentPath: string, fileTree: ChatResponseFileTree, baseUri: Uri): Promise<void | { success: boolean; errorMessage: string }> {
const itemPath = path.posix.join(parentPath, fileTree.name);
if (fileTree.children?.length || !looksLikeDirectory(fileTree.name)) {
files.push({ kind: 'qualifiedFile', uri: Uri.joinPath(baseUri, itemPath), fileContents: 'DIR' });
for (const item of fileTree.children ?? []) {
await traverseFileTree(itemPath, item, baseUri);
}
} else {
const result = await contentManager.get(Uri.joinPath(baseUri, itemPath))?.content;
const decoder = new TextDecoder();
const decodedString = decoder.decode(result);
if (!decodedString) {
return { success: false, errorMessage: `Content not found for ${itemPath}` };
}
files.push({ kind: 'qualifiedFile', uri: Uri.joinPath(baseUri, itemPath), fileContents: decodedString });
}
return;
}
for (const projectItem of projectItems.value) {
await traverseFileTree('', projectItem, projectItems.baseUri);
}
const tempDirPath = await createTempDir();
const projectRoot = path.join(tempDirPath, projectItems.baseUri.path);
await createTempWorkspace(tempDirPath, files);
const result = await compileWorkspace(accessor, projectRoot, files);
await cleanTempDir(tempDirPath);
return result;
}
}
return Promise.resolve({ success: false, errorMessage: 'Failed to parse new response' });
}
));
}
});
})();
// TODO @aiday-mar add possibility to execute python files and find the diagnostics or errors upon execution
async function compileWorkspace(accessor: ITestingServicesAccessor, projectRoot: string, files: IQualifiedFile[]): Promise<{ success: boolean; errorMessage?: string }> {
for (const file of files) {
const language = getLanguageForFile(file);
switch (language.languageId) {
case 'typescript':
{
// compute diagnostics
const tsDiagnostics = await compileTSWorkspace(accessor, projectRoot);
if (tsDiagnostics.length === 0) {
return { success: true };
}
const errors = tsDiagnostics.map(diagnostic => diagnostic.file + ' ' + diagnostic.code + ' : ' + diagnostic.message).join('\n');
return { success: false, errorMessage: 'Typescript diagnostics errors: \n' + errors };
}
}
}
return { success: true };
}
async function createTempWorkspace(tempDirPath: string, files: IQualifiedFile[]): Promise<void> {
await fs.promises.rm(tempDirPath, { recursive: true, force: true });
await fs.promises.mkdir(tempDirPath, { recursive: true });
for (const file of files) {
const tmpPath = path.join(tempDirPath, file.uri.path);
if (file.fileContents !== 'DIR') {
await fs.promises.writeFile(tmpPath, file.fileContents);
}
else {
await fs.promises.mkdir(tmpPath, { recursive: true });
}
}
}
@@ -1,230 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import { ContributedToolName, ToolName } from '../../src/extension/tools/common/toolNames';
import { getCellId } from '../../src/platform/notebook/common/helpers';
import { deserializeWorkbenchState } from '../../src/platform/test/node/promptContextModel';
import { ssuite, stest } from '../base/stest';
import { generateToolTestRunner } from './toolSimTest';
import { shouldSkipAgentTests } from './tools.stest';
ssuite.optional(shouldSkipAgentTests, {
title: 'notebooks', subtitle: 'toolCalling', location: 'panel', configurations: []
}, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-notebook-tools');
const getState = () => deserializeWorkbenchState(scenarioFolder, path.join(scenarioFolder, 'Chipotle1.state.json'));
stest('Run cell tool',
generateToolTestRunner({
scenarioFolderPath: scenarioFolder,
question: 'Run the first code cell.',
expectedToolCalls: { anyOf: [ToolName.RunNotebookCell, ToolName.GetNotebookSummary] },
getState,
tools: {
[ContributedToolName.GetNotebookSummary]: true,
[ContributedToolName.RunNotebookCell]: true
}
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.RunNotebookCell]: async (toolCalls) => {
const state = getState();
const activeDoc = state.activeTextEditor!.document!;
const solutionNotebook = state.notebookDocuments.find(doc => doc.uri.path === activeDoc.uri.path)!;
const codeCellIds = solutionNotebook?.getCells().filter(c => c.kind === 2).map(c => getCellId(c));
toolCalls.forEach((toolCall) => {
const cellId = (toolCall.input as { cellId: string }).cellId;
assert.ok(codeCellIds.includes(cellId), `Cell ${cellId} should be found in the notebook`);
});
},
[ToolName.GetNotebookSummary]: async () => {
// Ok to call this
},
[ToolName.EditNotebook]: async () => {
throw new Error('EditNotebook should not be called');
}
}
})
);
stest('New Notebook Tool with EditFile and EditNotebook',
generateToolTestRunner({
scenarioFolderPath: scenarioFolder,
question: `Create a new Jupyter Notebook using ${ContributedToolName.CreateNewJupyterNotebook} with 1 cell to that adds number 1 and 2.`,
expectedToolCalls: { anyOf: [ToolName.CreateNewJupyterNotebook, ToolName.EditFile, ToolName.EditNotebook] },
getState,
tools: {
[ContributedToolName.EditFile]: true,
[ContributedToolName.EditNotebook]: true,
[ContributedToolName.CreateNewJupyterNotebook]: true
}
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.EditNotebook]: async () => {
//
},
[ToolName.CreateNewJupyterNotebook]: async () => {
//
},
[ToolName.EditFile]: async () => {
//
}
}
})
);
stest('New Notebook Tool without EditFile and without EditNotebook',
generateToolTestRunner({
scenarioFolderPath: scenarioFolder,
question: `Create a new Jupyter Notebook using ${ContributedToolName.CreateNewJupyterNotebook} with 1 cell to that adds number 1 and 2.`,
expectedToolCalls: { anyOf: [ToolName.CreateNewJupyterNotebook] },
getState,
tools: {
[ContributedToolName.CreateNewJupyterNotebook]: true
}
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.EditNotebook]: async () => {
throw new Error('EditNotebook should not be called');
},
[ToolName.CreateNewJupyterNotebook]: async () => {
//
},
[ToolName.EditFile]: async () => {
throw new Error('EditFile should not be called');
}
}
})
);
stest('New Notebook Tool without EditFile and with EditNotebook',
generateToolTestRunner({
scenarioFolderPath: scenarioFolder,
question: `Create a new Jupyter Notebook using ${ContributedToolName.CreateNewJupyterNotebook} with 1 cell to that adds number 1 and 2.`,
expectedToolCalls: { anyOf: [ToolName.CreateNewJupyterNotebook] },
getState,
tools: {
[ContributedToolName.EditNotebook]: true,
[ContributedToolName.CreateNewJupyterNotebook]: true
}
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.EditNotebook]: async () => {
throw new Error('EditNotebook should not be called');
},
[ToolName.CreateNewJupyterNotebook]: async () => {
//
},
[ToolName.EditFile]: async () => {
throw new Error('EditFile should not be called');
}
}
})
);
stest('Run cell tool should avoid running markdown cells',
generateToolTestRunner({
scenarioFolderPath: scenarioFolder,
question: 'Run the first three cells.',
expectedToolCalls: { anyOf: [ToolName.RunNotebookCell, ToolName.GetNotebookSummary] },
getState,
tools: {
[ContributedToolName.GetNotebookSummary]: true,
[ContributedToolName.RunNotebookCell]: true
}
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.RunNotebookCell]: async (toolCalls) => {
const state = getState();
const activeDoc = state.activeTextEditor!.document!;
const solutionNotebook = state.notebookDocuments.find(doc => doc.uri.path === activeDoc.uri.path)!;
const first3CodeCells = solutionNotebook?.getCells().filter(c => c.kind === 2).map(c => getCellId(c)).slice(0, 3);
toolCalls.forEach((toolCall) => {
const cellId = (toolCall.input as { cellId: string }).cellId;
assert.ok(first3CodeCells.includes(cellId), `Cell ${cellId} was not one of the first three code cells`);
});
},
[ToolName.GetNotebookSummary]: async () => {
// Ok to call this
},
[ToolName.EditNotebook]: async () => {
throw new Error('EditNotebook should not be called');
}
}
})
);
stest('Run cell at a specific index',
generateToolTestRunner({
scenarioFolderPath: scenarioFolder,
question: 'Run the third cell.',
expectedToolCalls: { anyOf: [ToolName.RunNotebookCell, ToolName.GetNotebookSummary] },
getState,
tools: {
[ContributedToolName.GetNotebookSummary]: true,
[ContributedToolName.RunNotebookCell]: true
}
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.RunNotebookCell]: async (toolCalls) => {
const state = getState();
const activeDoc = state.activeTextEditor!.document!;
const solutionNotebook = state.notebookDocuments.find(doc => doc.uri.path === activeDoc.uri.path)!;
const thirdCell = solutionNotebook?.getCells()[2];
assert.equal(thirdCell?.kind, 2, 'Invalid test: The third cell should be a code cell');
toolCalls.forEach((toolCall) => {
const cellId = (toolCall.input as { cellId: string }).cellId;
assert.ok(thirdCell && getCellId(thirdCell) === cellId, `Cell ${cellId} should be the third code cell`);
});
},
[ToolName.GetNotebookSummary]: async () => {
// Ok to call this
},
[ToolName.EditNotebook]: async () => {
throw new Error('EditNotebook should not be called');
}
}
})
);
stest('Edit cell tool',
generateToolTestRunner({
scenarioFolderPath: scenarioFolder,
question: 'Change the header in the first markdown cell to "Hello Chipotle"',
expectedToolCalls: ToolName.EditNotebook,
getState,
tools: {
[ContributedToolName.GetNotebookSummary]: true, // Include this tool and verify that this isn't invoked (in the past this used to get invoked as part of editing).
}
}, {
allowParallelToolCalls: true,
toolCallValidators: {
[ToolName.RunNotebookCell]: async (toolCalls) => {
const state = getState();
const activeDoc = state.activeTextEditor!.document!;
const solutionNotebook = state.notebookDocuments.find(doc => doc.uri.path === activeDoc.uri.path)!;
toolCalls.forEach((toolCall) => {
const cellId = (toolCall.input as { cellId: string }).cellId;
const firstMarkdownCell = solutionNotebook?.getCells().find(c => c.kind === 1)!;
assert.equal(getCellId(firstMarkdownCell), cellId);
const newCode = (toolCall.input as { newCode: string[] }).newCode;
assert.notDeepEqual(newCode.indexOf('# Hello Chipotle'), -1, 'The first markdown cell should be changed to "Hello Chipotle"');
});
},
[ToolName.GetNotebookSummary]: async () => {
// Ok to call this
},
},
})
);
});
@@ -1,30 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { ssuite, stest } from '../base/stest';
import { pythonFixEvaluators } from './evaluators/pythonFix';
import { Scenario, fetchConversationScenarios } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
(function () {
ssuite({ title: 'fix', subtitle: 'python', location: 'panel' }, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-scenario-fix-python');
const scenarios: Scenario[] = fetchConversationScenarios(scenarioFolder);
// Dynamically create a test case per each entry in the scenarios array
for (let i = 0; i < scenarios.length; i++) {
const name = scenarios[i][0].name;
const label = name.replace('case', 'Case #').replace('.conversation.json', '');
stest({ description: label, language: 'python' },
generateScenarioTestRunner(
scenarios[i],
pythonFixEvaluators[scenarios[i][0].name]
));
}
});
})();
-279
View File
@@ -1,279 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 * as fs from 'fs';
import * as glob from 'glob';
import * as path from 'path';
import type { Command } from 'vscode';
import { Turn } from '../../src/extension/prompt/common/conversation';
import { ITestingServicesAccessor } from '../../src/platform/test/node/services';
import { ssuite, stest } from '../base/stest';
import { generateScenarioTestRunner } from './scenarioTest';
const NUM_SCENARIOS = 26;
/**
* Configuration object for a search test.
*/
interface ISearchTestConfig {
/**
* The question to ask the AI.
* ie: question: "find all links"
*/
question: string;
/**
* Whether the search query is a regular expression.
*/
isRegex: boolean;
/**
* Whether to search only in open editors.
*/
onlyOpenEditors?: boolean;
/**
* An array of expected include/exclude globs to use to target search files. This will be used to look for files to search, which will be compared to the list from the actual glob's include/exclude.
* ie:
* ```
* exampleIncludeGlobs: ["*.md", "*.html"]
* exampleExcludeGlobs: ["*.ts"]
* ```
*/
exampleIncludeGlobs?: string[];
exampleExcludeGlobs?: string[];
/**
* A map of search queries to the expected text results. If multiple are specified (2D string array), the test can match any of the possibilities.
* Might not include all files that gets searched.
* The actual results will be tested such that they CONTAIN the expected result. Therefore, the expected result should be the minimum-length possible result from the query.
* ie:
* queryShouldFind: "foo.md" -> ["http://google.ca", "http://github.com"]
* where the file "foo.md" matches the text "http://google.ca" and "http://github.com"
*
* or
*
* queryShouldFind: "foo.md" -> [["http://google.ca", "http://github.com"],["http://google.ca"]]
* where the file "foo.md" matches the text "http://google.ca" and "http://github.com" OR only "http://google.ca"
*/
queryShouldFind: Map<string, string[] | string[][]>;
/**
* A map of original file names to the file name of the file containing the expected replace result4
* ie:
* replaceResult: "foo.md" -> "foo.replaced.md"
*/
replaceResult?: Map<string, string>; // fileName original -> filename expected
/**
* Whether or not the answer should fail to give a query. Will be false by default.
* Would be true for an answer that says something like "I don't know what you're talking about, please clarify".
*/
shouldFail?: boolean;
}
interface ISearchArg {
filesToInclude?: string;
filesToExclude?: string;
query: string;
replace?: string;
isCaseSensitive?: boolean;
isRegex?: boolean;
matchWholeWord?: boolean;
onlyOpenEditors?: boolean;
preserveCase?: boolean;
}
interface ISimplifiedSearchArg {
filesToInclude: string;
filesToExclude: string;
query: string;
replace: string;
isRegex: boolean;
preserveCase: boolean;
onlyOpenEditors: boolean;
}
const scenarioFolder = path.join(__dirname, '..', 'test/scenarios/test-scenario-search/');
const exampleFolder = path.join(scenarioFolder, 'example-files');
const replaceSamples = path.join(scenarioFolder, 'replace-samples');
(function () {
ssuite({ title: 'search', location: 'panel' }, () => {
// Dynamically create a test case per each entry
for (let i = 0; i < NUM_SCENARIOS; i++) {
const testCase = getTestInfoFromFile(`search${i}.testArgs.json`);
const testName = testCase.question;
stest({ description: testName }, generateScenarioTestRunner(
[{ question: '@vscode /search ' + testCase.question, name: testName, scenarioFolderPath: scenarioFolder }],
generateEvaluate(testCase)
));
}
});
})();
function getTestInfoFromFile(fileName: string): ISearchTestConfig {
const file = path.join(scenarioFolder, fileName);
const fileContents = fs.readFileSync(file, 'utf8');
const json = JSON.parse(fileContents);
if (!json.queryShouldFind && !json.shouldFail) {
throw Error('Missing queryShouldFind field');
}
json.queryShouldFind = new Map(json.queryShouldFind);
if (json.replaceResult) {
json.replaceResult = new Map(json.replaceResult);
}
return json;
}
function generateEvaluate(testInfo: ISearchTestConfig) {
return async function evaluate(accessor: ITestingServicesAccessor, question: string, answer: string, _rawResponse: string, turn: Turn | undefined, _scenarioIndex: number, commands: Command[]): Promise<{ success: boolean; errorMessage?: string }> {
try {
let args: ISimplifiedSearchArg | undefined;
try {
args = createSimplifiedSearchArgs(await testArgs(commands));
} catch (e) {
if (testInfo.shouldFail) {
return Promise.resolve({ success: true, errorMessage: '' });
} else {
return Promise.resolve({ success: false, errorMessage: 'Parsing the search query failed.' });
}
}
if (testInfo.shouldFail) {
return Promise.resolve({ success: false, errorMessage: 'Parsing the search query should have failed.' });
}
assert(testInfo.isRegex === undefined || args.isRegex === testInfo.isRegex);
if (testInfo.onlyOpenEditors !== undefined) {
assert(args.onlyOpenEditors === testInfo.onlyOpenEditors);
}
if (!testInfo.replaceResult || testInfo.replaceResult.size === 0) {
assert(!args.replace);
}
const actualTargets = getTargetFiles(args.filesToInclude, args.filesToExclude);
const expectedTargets = getTargetFiles(testInfo.exampleIncludeGlobs ?? ['*'], testInfo.exampleExcludeGlobs ?? []);
assert.deepEqual(actualTargets, expectedTargets);
if (!args?.query) {
return Promise.resolve({ success: false, errorMessage: 'No query field on args' });
}
const query = args.query;
const preserveCase = args.preserveCase;
const replace = args.replace;
testInfo.queryShouldFind.forEach((expected, fileName) => {
const file = path.join(exampleFolder, fileName);
const results = testOnlyQueryOnFiles(file, query, preserveCase);
assert(resultMatchesQuery(results, expected));
});
testInfo.replaceResult?.forEach((fileNameExpected, fileName) => {
const file = path.join(exampleFolder, fileName);
const result = getStringFromReplace(file, query, replace, preserveCase);
const expected = fs.readFileSync(path.join(replaceSamples, fileNameExpected), 'utf8');
assert(result === expected);
});
} catch (e) {
const msg = (<any>e).message ?? 'Error: ' + e;
return Promise.resolve({ success: false, errorMessage: msg });
}
return Promise.resolve({ success: true, errorMessage: '' });
};
}
function getTargetFiles(fileGlobs: string | string[], ignoreGlobs: string | string[]): string[] {
if (!Array.isArray(fileGlobs)) {
fileGlobs = (fileGlobs.length === 0) ? ['*'] : fileGlobs.split(',');
}
if (!Array.isArray(ignoreGlobs)) {
ignoreGlobs = (ignoreGlobs.length === 0) ? [] : ignoreGlobs.split(',');
}
const included: string[] = [];
fileGlobs.forEach((fileGlob) => {
const matches = glob.sync(fileGlob, { cwd: exampleFolder, ignore: ignoreGlobs }).filter((file) =>
(!included.includes(file))
);
included.push(...matches);
});
return included;
}
function createSimplifiedSearchArgs(args: ISearchArg): ISimplifiedSearchArg {
return {
filesToInclude: args.filesToInclude ?? '',
filesToExclude: args.filesToExclude ?? '',
query: args.query,
replace: args.replace ?? '',
isRegex: args.isRegex ?? false,
preserveCase: args.preserveCase ?? false,
onlyOpenEditors: args.onlyOpenEditors ?? false
};
}
async function testArgs(commands: Command[]): Promise<ISearchArg> {
for (const c of commands) {
if (c.command === 'github.copilot.executeSearch') {
assert(c.title === 'Search');
return c.arguments?.[0];
}
}
throw Error('No search command found');
}
function getFunctionFromQuery(query: string, isCaseSensitive: boolean): RegExp {
const flags = isCaseSensitive ? 'gm' : 'gmi';
return new RegExp(query, flags);
}
function testOnlyQueryOnFiles(fileName: string, query: string, isCaseSensitive: boolean): string[] {
const file = fs.readFileSync(fileName, 'utf8');
const re = getFunctionFromQuery(query, isCaseSensitive);
const results = file.match(re)?.values();
return results ? Array.from(results) : [];
}
function getStringFromReplace(fileName: string, query: string, replace: string, isCaseSensitive: boolean): string {
const file = fs.readFileSync(fileName, 'utf8');
const re = getFunctionFromQuery(query, isCaseSensitive);
const str = file.replace(re, replace);
return str;
}
function resultMatchesQuery(actual: string[], expected: string[] | string[][]): boolean {
if (expected.length === 0) {
return (actual.length === 0);
}
const possibilitiesOfExpected: string[][] = (Array.isArray(expected[0]) ? expected : [expected]) as string[][];
const resultMatchesQuerySingle = (possibleExpected: string[]) => {
if (actual.length !== possibleExpected.length) {
return false;
}
for (let i = 0; i < actual.length; i++) {
if (!actual[i].includes(possibleExpected[i])) {
return false;
}
}
return true;
};
for (const expected of possibilitiesOfExpected) {
if (resultMatchesQuerySingle(expected)) {
return true;
}
}
return false;
}
@@ -1,67 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ssuite, stest } from '../base/stest';
import { discoverScenarios } from './scenarioLoader';
import { generateScenarioTestRunner, shouldSkip } from './scenarioTest';
function getFiles(answer: string): string[] {
const regex = /\#\#\s+(.*)\n/g;
let match;
const titles = [];
while ((match = regex.exec(answer)) !== null) {
titles.push(match[1].trim());
}
return [...new Set(titles)];
}
function expectedFileDoesMatch(files: string[], target: string): boolean {
return files.some(e => {
return e.trim().endsWith(target);
});
}
function assertFilesMatch(expected: string[], actual: string[]) {
expected.forEach(e => {
if (!expectedFileDoesMatch(actual, e)) {
throw Error(`Cannot find match for expected file: ${e}. Instead got the following: \n-${actual.join('\n')}`);
}
});
}
ssuite({ title: 'semanticSearch', location: 'panel' }, (inputPath) => {
// No default cases checked in at the moment
if (!inputPath) {
return;
}
const scenariosFolder = inputPath;
const scenarios = discoverScenarios(scenariosFolder);
for (const scenario of scenarios) {
const fileName = scenario[0].name;
const testName = fileName.substring(0, fileName.indexOf('.'));
stest.optional(shouldSkip.bind(undefined, scenario), { description: testName },
generateScenarioTestRunner(
scenario,
async (accessor, question, answer) => {
if (scenario[0].json.expectedRetrieval !== undefined) {
const expected: string[] = scenario[0].json.expectedRetrieval;
const actual = getFiles(answer);
try {
assertFilesMatch(expected, actual);
return { success: true, errorMessage: answer };
} catch (e) {
return { success: false, errorMessage: e.message };
}
// TODO: incorporate `keywords` into the test.
// They should already be on the tests, but to test solely file retrieval first, we can ignore them for now.
}
return { success: false, errorMessage: 'expectedRetrieval not defined' };
}));
}
});
@@ -1,74 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import type { TextSearchMatch2 } from 'vscode';
import { SemanticSearchTextSearchProvider } from '../../src/extension/workspaceSemanticSearch/node/semanticSearchTextSearchProvider';
import { IConversationOptions } from '../../src/platform/chat/common/conversationOptions';
import { TestingServiceCollection } from '../../src/platform/test/node/services';
import { SimulationWorkspace } from '../../src/platform/test/node/simulationWorkspace';
import { CancellationToken } from '../../src/util/vs/base/common/cancellation';
import { IInstantiationService } from '../../src/util/vs/platform/instantiation/common/instantiation';
import { ssuite, stest } from '../base/stest';
import { discoverScenarios } from './scenarioLoader';
import { fetchConversationOptions } from './scenarioTest';
ssuite({ title: 'semanticSearchView', location: 'panel' }, (inputPath) => {
// No default cases checked in at the moment
if (!inputPath) {
return;
}
if (inputPath.endsWith('tests')) {
inputPath = path.join(inputPath, 'semantic-search-view');
}
const scenariosFolder = inputPath;
const scenarios = discoverScenarios(scenariosFolder);
for (const scenario of scenarios) {
for (const testCase of scenario) {
stest({ description: 'Semantic search view: ' + testCase.question }, async (testingServiceCollection: TestingServiceCollection) => {
const workspaceState = testCase.getState ? testCase.getState() : undefined;
testingServiceCollection.define(IConversationOptions, fetchConversationOptions());
const simulationWorkspace = new SimulationWorkspace();
simulationWorkspace.setupServices(testingServiceCollection);
simulationWorkspace.resetFromDeserializedWorkspaceState(workspaceState);
const accessor = testingServiceCollection.createTestingAccessor();
const question = testCase.question;
const instantiationService = accessor.get(IInstantiationService);
const provider = instantiationService.createInstance(SemanticSearchTextSearchProvider);
const results: TextSearchMatch2[] = [];
await provider.provideAITextSearchResults(question, {
folderOptions: [],
maxResults: 1000,
previewOptions: {
matchLines: 100,
charsPerLine: 100
},
maxFileSize: undefined,
surroundingContext: 0
}, {
report: (value: TextSearchMatch2) => {
results.push(value);
}
}, CancellationToken.None);
if (testCase.json.keywords) {
for (const r of results) {
const matched = testCase.json.keywords.some((keyword: string) => r.previewText.includes(keyword));
if (matched) {
return { success: true };
}
}
}
assert.fail('No keywords found in results');
});
}
}
});
@@ -1,34 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { ssuite, stest } from '../base/stest';
import { validate } from '../base/validate';
import { fetchConversationScenarios } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
ssuite({ title: 'system', subtitle: 'identity', location: 'panel' }, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-system');
const scenarios = fetchConversationScenarios(scenarioFolder);
for (const scenario of scenarios) {
stest({ description: scenario[0].question, language: undefined }, generateScenarioTestRunner(
scenario,
async (accessor, question, answer) => {
const expectedKeywords = scenario.find((s) => s.question === question)?.json.keywords;
if (expectedKeywords !== undefined) {
const err = validate(answer, expectedKeywords);
if (err) {
return { success: false, errorMessage: err };
}
return { success: true, errorMessage: answer };
}
return { success: true, errorMessage: 'No requirements set for test.' };
}
));
}
});
@@ -1,605 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ok } from 'assert';
import { join } from 'path';
import { deserializeWorkbenchState } from '../../src/platform/test/node/promptContextModel';
import { ITestingServicesAccessor } from '../../src/platform/test/node/services';
import { extractCodeBlocks, extractInlineCode } from '../../src/util/common/markdown';
import { ssuite, stest } from '../base/stest';
import { ScenarioEvaluator } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
const scenarioFolder = join(__dirname, '..', 'test/scenarios/test-terminal/');
type SupportedShellType = 'bash' | 'fish' | 'powershell' | 'zsh';
type TerminalTestCaseSingleAnswer = (string | RegExp)[];
type TerminalTestCaseShellSpecificAnswer = (
// Shell-specific answers
Partial<Record<SupportedShellType, TerminalTestCaseSingleAnswer>>
&
// Negated shell-specific answers
Partial<Record<`!${SupportedShellType}`, TerminalTestCaseSingleAnswer>>
&
(
{
// Answers that apply to any shell
any?: TerminalTestCaseSingleAnswer;
// The fallback default answer when no shell-specific answer exists
default: TerminalTestCaseSingleAnswer;
}
|
{
// Answers that apply to any shell
any: TerminalTestCaseSingleAnswer;
// The fallback default answer when no shell-specific answer exists
default?: TerminalTestCaseSingleAnswer;
}
)
);
interface ITerminalTestCase {
/**
* The question being asked.
*/
question: string;
/**
* The shell type context.
*/
shellType: SupportedShellType;
/**
* The ideal answer(s) to the question.
*/
bestAnswer: TerminalTestCaseSingleAnswer | TerminalTestCaseShellSpecificAnswer;
/**
* Answers that are acceptable but not ideal, for example they may do what asked but in a more
* complicated way than necessary or require slight user tweaks.
*/
acceptableAnswers?: TerminalTestCaseSingleAnswer | Partial<TerminalTestCaseShellSpecificAnswer>;
}
const supportedShells = [
'bash',
'fish',
'powershell',
'zsh'
] as const;
function getShellSpecificAnswer(answerObject: TerminalTestCaseSingleAnswer | TerminalTestCaseShellSpecificAnswer | Partial<TerminalTestCaseShellSpecificAnswer>, shellType: SupportedShellType): TerminalTestCaseSingleAnswer {
// No shell-specific answers
if (Array.isArray(answerObject)) {
return answerObject;
}
let answer: TerminalTestCaseSingleAnswer;
if (shellType in answerObject) {
answer = [...answerObject[shellType]!];
} else {
answer = answerObject.default ? [...answerObject.default] : [];
}
if ('any' in answerObject) {
answer.push(...answerObject.any!);
}
for (const negatedShellType in supportedShells) {
if (shellType === negatedShellType) {
continue;
}
if (`!${negatedShellType}` in answerObject) {
answer.push(...answerObject[`!${negatedShellType as SupportedShellType}`]!);
}
}
return answer;
}
const generalTestCases: ITerminalTestCase[] = [];
for (const shellType of supportedShells) {
generalTestCases.push(...([
{
shellType, question: 'go to the foo dir',
bestAnswer: {
powershell: [
/Set-Location( -Path)? (\.\\foo\\|(\.\/)?foo\/?)/,
/cd (\.\\foo\\|(\.\/)?foo\/?)/
],
default: [
'cd foo'
]
},
},
{
shellType, question: 'print the directory',
bestAnswer: {
powershell: [
'Get-Location'
],
default: [
'pwd'
]
},
acceptableAnswers: {
powershell: [
'pwd'
]
}
},
{
shellType, question: 'print README.md',
bestAnswer: {
powershell: [
'Get-Content README.md',
],
any: [
'cat README.md',
]
},
},
{
shellType, question: 'list files in directory',
bestAnswer: {
powershell: [
'Get-ChildItem',
/Get-ChildItem -Path .\\?/
],
any: [
'ls'
]
},
acceptableAnswers: {
powershell: [
/Get-ChildItem -Path {.+}/
]
},
},
{
shellType, question: 'create a file called foo',
bestAnswer: {
powershell: [
/New-Item( -ItemType File)? -Name "?foo"?/
],
default: [
'touch foo'
]
},
},
{
shellType, question: 'delete the foo.txt file',
bestAnswer: {
powershell: [
/Remove-Item (\.[\\/])?foo.txt/
],
default: [
'rm foo.txt'
]
},
},
{
shellType, question: 'delete the foo/ dir',
bestAnswer: {
powershell: [
/Remove-Item( -Recurse| -Force)*( -Path)? (\.\\foo\\|(\.\/)?foo\/?)( -Recurse| -Force)*/
],
default: [
/rm -rf? foo\/?/
]
},
},
{
shellType, question: 'create a symlink',
bestAnswer: {
powershell: [
/New-Item -ItemType SymbolicLink -Path "?{.+}"? -(Target|Value) "?{.+}"?/
],
default: [
/ln -s {.+} {.+}/
]
},
},
{
shellType, question: 'print "hello world"',
bestAnswer: {
powershell: [
/(echo|Write-(Host|Output)) "[hH]ello [wW]orld"/
],
default: [
/echo "[hH]ello [wW]orld"/
]
},
},
{
shellType, question: 'kill the process using port 8123',
bestAnswer: {
powershell: [
/Stop-Process -Id \(Get-NetTCPConnection -LocalPort 8123\).OwningProcess( -Force)?/,
/Get-NetTCPConnection -LocalPort 8123 \| ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force }/
],
fish: [
/fuser -k 8123\/tcp/,
/(sudo )?kill (-9 )?\((sudo )?lsof -t( -i|i) ?(tcp)?:8123\)/,
],
default: [
/fuser -k 8123\/tcp/,
/(sudo )?kill (-9 )?\$\((sudo )?lsof -t -i:8123\)/,
/(sudo )?lsof -t -i:8123 \| xargs kill -9/,
/(sudo )?lsof -ti:8123 \| xargs kill -9/,
/(sudo )?lsof -i :8123 \| awk 'NR!=1 {print \$2}/,
]
},
acceptableAnswers: {
powershell: [
/Stop-Process -Id \(Get-NetTCPConnection -LocalPort {.+}\).OwningProcess -Force/
],
fish: [
/kill (-9 )?\(lsof -t -i\s?:{.+}\)/,
],
default: [
/(sudo )?kill (-9 )?\$\(lsof -t -i:{.+}\)/,
/(sudo )?lsof -ti:{.+} \| xargs kill -9/,
]
}
},
{
shellType, question: 'kill process using port',
bestAnswer: {
powershell: [
/Get-NetTCPConnection \| Where-Object LocalPort -eq {.+} \| ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force }/,
/Get-NetTCPConnection -LocalPort {.+} \| ForEach-Object { Stop-Process -Id \$_.OwningProcess/,
/Stop-Process -Id \(Get-NetTCPConnection -LocalPort {.+}\).OwningProcess( -Force)?/
],
fish: [
/fuser -k {.+}\/tcp/,
/(sudo )?kill (-9 )?\(lsof -t(i)?\s?:{.+}\)/,
/(sudo )?kill (-9 )?\(lsof -t -i\s?:{.+}\)/
],
default: [
/fuser -k {.+}\/tcp/,
/(sudo )?kill (-9 )?\$\((sudo )?lsof -t -i:{.+}\)/,
/lsof -ti:{.+} \| xargs kill -9/,
]
},
},
{
shellType, question: 'extract a tar file',
bestAnswer: [
/tar -xv?f {.+}/,
],
},
{
shellType, question: 'extract foo.tar',
bestAnswer: {
powershell: [
/Expand-Archive( -Path)? ['"]?foo.tar['"]? -DestinationPath ['"]?(\.|{.+})[\\\/]?['"]?/
],
default: [
/tar -xv?f foo.tar/,
]
},
acceptableAnswers: {
powershell: [
/tar -xv?f foo.tar/,
]
}
},
{
shellType, question: 'extract a zip file',
bestAnswer: {
powershell: [
/Expand-Archive( -Path)? ['"]?{.+}['"]? -DestinationPath ['"]?(\.|{.+})[\\\/]?['"]?/
],
default: [
/unzip {.+}/
]
},
},
{
shellType, question: 'extract foo.zip',
bestAnswer: {
powershell: [
/Expand-Archive( -Path)? (\.[\\/])?foo.zip -DestinationPath (\.|{.+})[\\\/]?/
],
default: [
'unzip foo.zip'
]
},
},
{
shellType, question: 'extract foo.tar to bar/',
bestAnswer: {
powershell: [
/Expand-Archive( -Path)? foo.tar -DestinationPath bar/
],
default: [
/tar -xv?f foo.tar -C bar\//,
]
},
acceptableAnswers: {
powershell: [
'tar -xf foo.tar -C bar/'
]
}
},
{
shellType, question: 'make a directory',
bestAnswer: {
powershell: [
/New-Item (-Path \. )?-Name "{.+}" -ItemType Directory/,
/New-Item -ItemType Directory (-Path \. )?-Name "?{.+}"?/
],
any: [
/mkdir {.+}/
]
},
},
{
shellType, question: 'make a directory called foo',
bestAnswer: {
powershell: [
/New-Item (-Path \. )?-Name foo -ItemType Directory/,
/New-Item -ItemType Directory (-Path \. )?-Name foo/
],
default: [
/mkdir foo/
]
},
acceptableAnswers: {
powershell: [
/mkdir foo/,
]
}
},
{
shellType, question: 'copy file foo to bar/',
bestAnswer: {
powershell: [
/Copy-Item (-Path )?(\.\\bar|(\.\/)?foo) (-Destination )?(\.\\bar\\|(\.\/)?bar\/?)/,
/cp (\.\\bar|(\.\/)?foo) (\.\\bar\\|(\.\/)?bar\/?)/,
],
default: [
'cp foo bar/'
]
},
},
{
shellType, question: 'move file foo to bar/',
bestAnswer: {
powershell: [
/Move-Item (-Path )?(\.[\\/])?foo (-Destination )?(\.\\bar\\|(\.\/)?bar\/?)/,
/mv (\.[\\/])?foo (\.\\bar\\|(\.\/)?bar\/?)/
],
default: [
'mv foo bar/'
]
},
},
{
shellType, question: 'kill the visual studio code process',
bestAnswer: {
powershell: [
/Stop-Process -Name "?[cC]ode"?/
],
fish: [
/pkill( -f)? "?code"?/,
'kill (pidof code)',
/kill \(pgrep [cC]ode\)/,
/killall (vscode|[cC]ode|["']Visual Studio Code["'])/,
],
default: [
/pkill( -f)? "?code"?/,
/pkill -f ["']Visual Studio Code["']/,
/killall (vscode|[cC]ode|["']Visual Studio Code["'])/,
'kill $(pgrep code)',
/kill \$\(pgrep -f ["']Visual Studio Code["']\)/,
]
},
acceptableAnswers: {
'!powershell': [
/pkill( -f)? "?code"?/,
]
},
},
{
shellType, question: 'how do i download a file',
bestAnswer: {
powershell: [
/Invoke-WebRequest -Uri "?{.+}"? -OutFile "?{.+}"?/
],
default: [
/wget {.+}/,
/curl -O {.+}/,
]
},
},
{
shellType, question: 'how do i download a file using curl',
bestAnswer: [
/curl -O {.+}/,
/curl -o {.+} {.+}/,
],
},
] satisfies ITerminalTestCase[]));
}
// zsh-specific
generalTestCases.push(...([
{
shellType: 'zsh',
question: 'turn off the zsh git plugin',
bestAnswer: [
/\.zshrc/ // The answer must include a reference to .zshrc
]
},
] satisfies ITerminalTestCase[]));
// Git test cases
const gitTestCases: ITerminalTestCase[] = [];
for (const shellType of supportedShells) {
gitTestCases.push(...[
{
shellType,
question: 'show last git commit details',
bestAnswer: [
'git show',
'git show HEAD',
'git show --summary',
'git show -1',
/git show --oneline( -1| -s HEAD)/,
],
acceptableAnswers: [
'git show --stat',
'git log -1',
],
},
{
shellType,
question: 'list all git commits by Daniel',
bestAnswer: [
'git log --author=Daniel',
'git log --author="Daniel"',
],
},
{
shellType,
question: 'enable colors in the git cli',
bestAnswer: [
'git config --global color.ui auto',
'git config --global color.ui true',
]
},
{
shellType,
question: 'checkout the foo branch',
bestAnswer: [
'git checkout foo',
]
},
{
shellType,
question: 'create and checkout the foo branch',
bestAnswer: [
'git checkout -b foo',
]
},
{
shellType,
question: 'merge the branch foo into this branch',
bestAnswer: [
'git merge foo',
]
},
{
shellType,
question: 'delete the foo branch',
bestAnswer: [
'git branch -d foo',
]
},
{
shellType,
question: 'create a git repo in this folder',
bestAnswer: [
'git init',
]
},
{
shellType,
question: 'add a git remote',
bestAnswer: [
/git remote add {.+} {.+}/,
]
},
]);
}
for (const { title, testCases } of [
{ title: 'general', testCases: generalTestCases },
{ title: 'git', testCases: gitTestCases },
]) {
ssuite({ title: `terminal (${title})`, location: 'panel' }, () => {
for (const testCase of testCases) {
// Non-strict tests verify _any expected_ answer was given in _any_ code block
stest(
{
description: testCase.question,
language: testCase.shellType,
},
generateScenarioTestRunner(
[{
question: `@terminal ${testCase.question}`,
name: testCase.question,
scenarioFolderPath: scenarioFolder,
getState: () => deserializeWorkbenchState(scenarioFolder, join(scenarioFolder, `${testCase.shellType ?? 'bash'}.state.json`)),
}],
generateEvaluate(testCase)
)
);
// Strict tests verify the _best expected_ answer was given and the _first_ code block
stest(
{
description: `${testCase.question} (strict)`,
language: testCase.shellType,
},
generateScenarioTestRunner(
[{
question: `@terminal ${testCase.question}`,
name: `${testCase.question} (strict)`,
scenarioFolderPath: scenarioFolder,
getState: () => deserializeWorkbenchState(scenarioFolder, join(scenarioFolder, `${testCase.shellType ?? 'bash'}.state.json`)),
}],
generateEvaluate(testCase, { strict: true })
)
);
}
});
}
interface IGenerateEvaluateOptions {
strict?: boolean;
}
function generateEvaluate(testCase: ITerminalTestCase, options: IGenerateEvaluateOptions = {}): ScenarioEvaluator {
return async function evaluate(accessor: ITestingServicesAccessor, question: string, answer: string): Promise<{ success: boolean; errorMessage?: string }> {
const inlineCode = extractInlineCode(answer);
const codeBlocks = extractCodeBlocks(answer);
const commandSuggestions = codeBlocks.map(e => e.code);
// Only include inline code suggestions in strict tests as it's harder to action inline code
if (!options.strict) {
commandSuggestions.concat(inlineCode);
}
if (options.strict) {
const firstSuggestion = commandSuggestions[0];
const bestAnswer = getShellSpecificAnswer(testCase.bestAnswer, testCase.shellType);
ok(
firstSuggestion !== undefined && bestAnswer.some(e => {
return (typeof e === 'string'
? e === firstSuggestion
: firstSuggestion.match(e)
);
}),
`The _first_ code block (\`${commandSuggestions[0]}\`) should _equal_ the expected answer (\`${bestAnswer.join(',')}\`)`
);
} else {
const bestAnswer = getShellSpecificAnswer(testCase.bestAnswer, testCase.shellType);
const acceptableAnswers = [...bestAnswer];
if (testCase.acceptableAnswers) {
acceptableAnswers.push(...getShellSpecificAnswer(testCase.acceptableAnswers, testCase.shellType));
}
ok(
commandSuggestions.some(e => {
return acceptableAnswers.some(expected => {
return (typeof expected === 'string'
? e.includes(expected)
: e.match(expected)
);
});
}),
`Any code block or inline code should _include_ an expected answer (\`${acceptableAnswers.join(',')}\`)`
);
}
return Promise.resolve({ success: true, errorMessage: '' });
};
}
@@ -1,23 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import path from 'path';
import { ssuite, stest } from '../base/stest';
import { fetchToolScenarios, generateToolTestRunner } from './toolSimTest';
export function shouldSkipAgentTests(): boolean {
return process.env.AGENT_TESTS !== '1';
}
ssuite.optional(shouldSkipAgentTests, { title: 'toolCalling', location: 'panel' }, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-tools');
const scenarios = fetchToolScenarios(scenarioFolder);
for (const scenario of scenarios) {
stest(scenario[0].question,
generateToolTestRunner(scenario));
}
});
@@ -1,43 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { extractCodeBlocks } from '../../src/util/common/markdown';
import { ssuite, stest } from '../base/stest';
import { fetchConversationScenarios, Scenario } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
interface IExtendedTestData {
contentInOutput: string;
}
(function () {
ssuite({ title: 'fix', subtitle: 'typescript', location: 'panel' }, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-scenario-fix-typescript');
const scenarios: Scenario[] = fetchConversationScenarios(scenarioFolder);
// Dynamically create a test case per each entry in the scenarios array
for (let i = 0; i < scenarios.length; i++) {
const scenario = scenarios[i][0];
stest({ description: scenario.name, language: 'typescript' },
generateScenarioTestRunner(
scenarios[i],
async (accessor, question, response) => {
const codeBlock = extractCodeBlocks(response).at(0);
if (!codeBlock || codeBlock.language !== 'typescript') {
return { success: false, errorMessage: 'No typescript code block found in response' };
}
const contentInOutput = (scenario.json as IExtendedTestData).contentInOutput;
if (!codeBlock.code.includes(contentInOutput)) {
return { success: false, errorMessage: `Typescript code block does not contain ${contentInOutput}` };
}
return { success: true };
}
)
);
}
});
})();
@@ -1,35 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { getLanguage } from '../../src/util/common/languages';
import { ssuite, stest } from '../base/stest';
import { validate } from '../base/validate';
import { fetchConversationScenarios } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
ssuite({ title: 'variables', location: 'panel' }, (inputPath) => {
const scenarioFolder = inputPath ?? path.join(__dirname, '..', 'test/scenarios/test-variables');
const scenarios = fetchConversationScenarios(scenarioFolder);
for (const scenario of scenarios) {
const language = scenario[0].getState?.().activeTextEditor?.document.languageId;
stest({ description: scenario[0].json.description ?? scenario[0].question, language: language ? getLanguage(language).languageId : undefined }, generateScenarioTestRunner(
scenario,
async (accessor, question, answer) => {
if (scenario[0].json.keywords !== undefined) {
const err = validate(answer, scenario[0].json.keywords);
if (err) {
return { success: false, errorMessage: err };
}
return { success: true };
}
return { success: true, errorMessage: 'No requirements set for test.' };
}
));
}
});
@@ -1,64 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ChatVariablesCollection } from '../../src/extension/prompt/common/chatVariablesCollection';
import { VscodePrompt } from '../../src/extension/prompts/node/panel/vscode';
import { IEndpointProvider } from '../../src/platform/endpoint/common/endpointProvider';
import { CancellationToken } from '../../src/util/vs/base/common/cancellation';
import { IInstantiationService } from '../../src/util/vs/platform/instantiation/common/instantiation';
import { ssuite, stest } from '../base/stest';
ssuite.skip({ title: 'vscode', subtitle: 'metaprompt', location: 'panel' }, async (_) => {
const scenarios = [
{
question: 'how to opne command pallete',
keywords: ['open', 'command palette'],
excludedKeywords: ['how',]
},
{
question: 'how do I change font size setting',
keywords: ['setting', 'font size'],
excludedKeywords: ['how',]
},
{
question: 'enable word wrap in editer',
keywords: ['enable', 'editor', 'word wrap'],
excludedKeywords: []
},
];
for (const scenario of scenarios) {
stest({ description: scenario.question },
async (testingServiceCollection) => {
const accessor = testingServiceCollection.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const endpoint = await accessor.get(IEndpointProvider).getChatEndpoint('copilot-utility');
const vscodePrompt = instantiationService.createInstance(VscodePrompt, {
promptContext: {
chatVariables: new ChatVariablesCollection([]),
history: [],
query: scenario.question,
},
endpoint
});
const tokenizer = endpoint.acquireTokenizer();
const countTokens = (text: string) => tokenizer.tokenLength(text);
const prompt = await vscodePrompt.prepare({ tokenBudget: 2048, endpoint, countTokens }, undefined, CancellationToken.None);
assert.notEqual(prompt.query, scenario.question);
for (const keyword of scenario.keywords) {
assert.ok(prompt.query.toLowerCase().includes(keyword.toLowerCase()), `${keyword} not found in prompt query`);
}
for (const keyword of scenario.excludedKeywords) {
assert.ok(!prompt.query.toLowerCase().includes(keyword.toLowerCase()), `prompt query should not include "${keyword}"`);
}
});
}
});
@@ -1,41 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ssuite, stest } from '../base/stest';
import { validate } from '../base/validate';
import { discoverScenarios } from './scenarioLoader';
import { generateScenarioTestRunner } from './scenarioTest';
ssuite({ title: '@vscode', location: 'panel' }, (inputPath) => {
if (!inputPath) {
return;
}
const scenarios = discoverScenarios(inputPath);
for (const scenario of scenarios) {
const fileName = scenario[0].name;
const testName = inputPath ? fileName.substring(0, fileName.indexOf('.')) : scenario[0].question.replace('@vscode', '');
stest({ description: testName }, generateScenarioTestRunner(
scenario,
async (accessor, question, answer, rawResponse, turn, scenarioIndex, commands) => {
if (scenario[0].json.keywords !== undefined) {
const err = validate(rawResponse, scenario[0].json.keywords);
if (err) {
return { success: false, errorMessage: err };
}
const showCommands = scenario[0].json.showCommand as boolean ?? true;
if (showCommands && commands.length === 0) {
return { success: false, errorMessage: 'Response is missing required commands.' };
} else if (!showCommands && commands.length > 0) {
return { success: false, errorMessage: 'Response includes commands that should not be present.' };
}
return { success: true, errorMessage: answer };
}
return { success: true, errorMessage: 'No requirements set for test.' };
}
));
}
});
@@ -1,38 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ssuite, stest } from '../base/stest';
import { validate } from '../base/validate';
import { discoverScenarios } from './scenarioLoader';
import { generateScenarioTestRunner, shouldSkip } from './scenarioTest';
ssuite({ title: 'workspace', subtitle: 'e2e', location: 'panel' }, (inputPath) => {
// No default cases checked in at the moment
if (!inputPath) {
return;
}
const scenariosFolder = inputPath;
const scenarios = discoverScenarios(scenariosFolder);
for (const scenario of scenarios) {
const fileName = scenario[0].name;
const testName = fileName.substring(0, fileName.indexOf('.'));
stest.optional(shouldSkip.bind(undefined, scenario), { description: testName },
generateScenarioTestRunner(
scenario,
async (accessor, question, answer) => {
if (scenario[0].json.keywords !== undefined) {
const err = validate(answer, scenario[0].json.keywords);
if (err) {
return { success: false, errorMessage: err };
}
return { success: true, errorMessage: answer };
}
return { success: true, errorMessage: 'No requirements set for test.' };
}));
}
});
@@ -1,60 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { CHAT_MODEL } from '../../src/platform/configuration/common/configurationService';
import { TestingServiceCollection } from '../../src/platform/test/node/services';
import { ssuite, stest } from '../base/stest';
import { assertFileContent, assertNoElidedCodeComments, getWorkspaceDiagnostics } from '../simulation/outcomeValidators';
import { EditTestStrategyPanel, simulatePanelCodeMapper } from '../simulation/panelCodeMapperSimulator';
import { assertWorkspaceEdit, fromFixture } from '../simulation/stestUtil';
import { EditTestStrategy, IScenario } from '../simulation/types';
function executeEditTest(
strategy: EditTestStrategyPanel,
testingServiceCollection: TestingServiceCollection,
scenario: IScenario
): Promise<void> {
return simulatePanelCodeMapper(testingServiceCollection, scenario, strategy);
}
function forAgent(callback: (model: string | undefined) => void): void {
callback(undefined);
callback(CHAT_MODEL.CLAUDE_SONNET);
}
const skipAgentTests = true;
forAgent((model) => {
const title = model ? `edit-agent-${model}` : 'edit-agent';
ssuite.optional(() => skipAgentTests, { title, location: 'panel' }, () => {
stest({ description: 'issue #8098: extract function to unseen file', language: 'typescript', model }, (testingServiceCollection) => {
return executeEditTest(EditTestStrategy.Agent, testingServiceCollection, {
files: [
fromFixture('multiFileEdit/issue-8098/debugUtils.ts'),
fromFixture('multiFileEdit/issue-8098/debugTelemetry.ts'),
],
queries: [
{
query: 'Extract filterExceptionsFromTelemetry to debugTelemetry.ts',
validate: async (outcome, workspace, accessor) => {
assertWorkspaceEdit(outcome);
assert.ok(outcome.files.length === 2, 'Expected two files to be edited');
const utilsTs = assertFileContent(outcome.files, 'debugUtils.ts');
assert.ok(!utilsTs.includes('function filterExceptionsFromTelemetry'), 'Expected filterExceptionsFromTelemetry to be extracted');
const telemetryFile = assertFileContent(outcome.files, 'debugTelemetry.ts');
assert.ok(telemetryFile.includes('filterExceptionsFromTelemetry'), 'Expected filterExceptionsFromTelemetry to be extracted');
assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);
assertNoElidedCodeComments(outcome);
}
}
]
});
});
});
});
Loaded 100 of 559 files, more files were not shown because too many files have changed in this diff. Show more