Merge origin/main into chrmarti/bad-earthworm

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Christof Marti
2026-08-27 12:22:28 +02:00
co-authored by Copilot
748 changed files with 39670 additions and 10348 deletions
@@ -13,11 +13,10 @@ applyTo: src/vs/**/*.css
> vocabulary, worked examples, and how to give UI feedback in design terms.
VS Code ships a design-system **size** ramp. These tokens are registered in
[baseSizes.ts](../../src/vs/platform/theme/common/sizes/baseSizes.ts) (and the
agents font ramp in [sizes.ts](../../src/vs/sessions/common/sizes.ts)) and are
emitted as `--vscode-*` CSS variables. **When generating or editing CSS, use the
token variable instead of a raw `px` value** wherever a token exists for that
value. This keeps new UI visually consistent with the design system.
[baseSizes.ts](../../src/vs/platform/theme/common/sizes/baseSizes.ts) and emitted
as `--vscode-*` CSS variables. **When generating or editing CSS, use the token
variable instead of a raw `px` value** wherever a token exists for that value.
This keeps new UI visually consistent with the design system.
> Every `--vscode-*` size var you reference must already exist in
> [vscode-known-variables.json](../../build/lib/stylelint/vscode-known-variables.json)
@@ -98,50 +97,44 @@ reuses the matching size token + `fontWeight.semiBold`, **never** a separate
| 11 | `--vscode-fontSize-label2` | regular |
| 10 | `--vscode-fontSize-label3` | regular |
**Deprecated** — the legacy `--vscode-bodyFontSize*` tokens are deprecated. Use
the generic ramp above instead:
**Deprecated** — the legacy `--vscode-bodyFontSize*` and Agents-specific
`--vscode-agents-fontSize-*` tokens are deprecated. Use the generic ramp above
instead:
| Deprecated | px | Use instead |
|------------|----|-------------|
| `--vscode-bodyFontSize` | 13 | `--vscode-fontSize-body1` |
| `--vscode-bodyFontSize-small` | 12 | `--vscode-fontSize-label1` |
| `--vscode-bodyFontSize-xSmall` | 11 | `--vscode-fontSize-body2` |
Agents window ramp (`src/vs/sessions/**`) — identical values, `agents-`-prefixed
(pair size with a weight token, **never** add a separate "strong" size):
| px | Size var | Weight |
|----|----------|--------|
| 26 | `--vscode-agents-fontSize-heading1` | semiBold |
| 18 | `--vscode-agents-fontSize-heading2` | semiBold |
| 13 | `--vscode-agents-fontSize-heading3` | semiBold |
| 13 | `--vscode-agents-fontSize-body1` | regular |
| 11 | `--vscode-agents-fontSize-body2` | regular |
| 12 | `--vscode-agents-fontSize-label1` | regular |
| 11 | `--vscode-agents-fontSize-label2` | regular |
| 10 | `--vscode-agents-fontSize-label3` | regular |
Weights: `--vscode-agents-fontWeight-regular` (400),
`--vscode-agents-fontWeight-semiBold` (600). The ramp is **400/600 only** — there
is no medium (500). "Strong" = same size token + `semiBold`. See
[Font weight](#font-weight--font-weight) below.
| `--vscode-agents-fontSize-heading1` | 26 | `--vscode-fontSize-heading1` |
| `--vscode-agents-fontSize-heading2` | 18 | `--vscode-fontSize-heading2` |
| `--vscode-agents-fontSize-heading3` | 13 | `--vscode-fontSize-heading3` |
| `--vscode-agents-fontSize-body1` | 13 | `--vscode-fontSize-body1` |
| `--vscode-agents-fontSize-body2` | 11 | `--vscode-fontSize-body2` |
| `--vscode-agents-fontSize-label1` | 12 | `--vscode-fontSize-label1` |
| `--vscode-agents-fontSize-label2` | 11 | `--vscode-fontSize-label2` |
| `--vscode-agents-fontSize-label3` | 10 | `--vscode-fontSize-label3` |
## Font weight — `font-weight`
Both the generic and agents ramps use a **two-weight ramp** — there are no other
weights. Pair every text style with one of these:
The generic ramp uses two weights — there are no others. Pair every text style
with one of these:
| weight | Generic var | Agents var | Use |
|--------|-------------|------------|-----|
| 400 | `--vscode-fontWeight-regular` | `--vscode-agents-fontWeight-regular` | body, labels, metadata |
| 600 | `--vscode-fontWeight-semiBold` | `--vscode-agents-fontWeight-semiBold` | headings, "strong" emphasis |
| weight | Variable | Use |
|--------|----------|-----|
| 400 | `--vscode-fontWeight-regular` | body, labels, metadata |
| 600 | `--vscode-fontWeight-semiBold` | headings, "strong" emphasis |
The legacy `--vscode-agents-fontWeight-regular` and
`--vscode-agents-fontWeight-semiBold` tokens are deprecated; use the corresponding
generic variables above.
- **No medium (500).** `font-weight: 500` is **off the ramp** — snap it to
`semiBold` (600). The same goes for `700`/`bold` and any other numeric weight:
round to the nearer of 400/600.
- **"Strong" is not a separate size.** A "Body 1 Strong" / "Label 2 Strong"
style reuses the matching `--vscode-fontSize-*` (or `--vscode-agents-fontSize-*`)
token paired with `semiBold`. Never introduce a separate strong *size* token.
style reuses the matching `--vscode-fontSize-*` token paired with `semiBold`.
Never introduce a separate strong *size* token.
- `normal` ≡ 400 → `regular`. **Leave untouched:** `inherit`, `lighter`,
`bolder`, and any `var()`/`calc()` expression. Preserve `!important`.
+3 -4
View File
@@ -257,10 +257,9 @@ vocabulary that lets an agreed design be built consistently**, not as the openin
move in a review. Reach for them *after* you've named the feeling and the
principle, never instead of it.
The size tokens live in
[`baseSizes.ts`](../../../src/vs/platform/theme/common/sizes/baseSizes.ts) and the
font ramp in [`sizes.ts`](../../../src/vs/sessions/common/sizes.ts); the full
reference is in
The size and font tokens live in
[`baseSizes.ts`](../../../src/vs/platform/theme/common/sizes/baseSizes.ts); the
full reference is in
[design-tokens.instructions.md](../../instructions/design-tokens.instructions.md).
<a id="design-tokens"></a>
@@ -81,7 +81,7 @@ the schema's nested
| Schema property (path) | Type in schema | Composition (`x-composition.strategy`) |
|------------------------|----------------|----------------------------------------|
| `permissions.disableBypassPermissionsMode` | string enum `"disable"` | most-restrictive-wins (sticky once set) |
| `permissions.disableBypassPermissionsMode` | string enum `"disable"` \| `"allow-auto-only"` | most-restrictive-wins (sticky once set) |
| `model` | string (`auto`, a model family name, or a full model id) | — |
| `permissions.model` | string (legacy location for `model`) | — |
| `forceRemoteSettingsRefresh` | boolean | MDM wins; controls the server cache rather than a configuration setting |
@@ -372,10 +372,13 @@ constant, configuration policy, or policy-data export.
`forceRemoteSettingsRefresh` is not a user configuration setting. It controls whether the
server-managed-settings cache may satisfy startup, so VS Code preserves it in the cached raw server
bag and always includes it in the native MDM watch schema. `DefaultAccountProvider` resolves an
explicit native MDM boolean ahead of the cached server value; when the result is `true`, it bypasses
an otherwise-fresh server cache for the first fetch for that account in the current process. The
cache remains available as the normal fetch-failure fallback.
bag and always includes it in the native MDM watch schema. `DefaultAccountProvider` resolves the
control across native MDM, cached server, and managed-file delivery before using the server cache.
When the result is `true`, only a fresh successful server response for the current account,
authentication provider, and endpoint satisfies the requirement. A failed refresh may retain cached
restrictions and the flag itself, but the Account Policy gate keeps AI features disabled until a
retry succeeds. Authentication remains available so users can recover from missing or expired
credentials.
Reference tests:
- `src/vs/platform/policy/test/common/copilotManagedSettings.test.ts`
@@ -25,11 +25,22 @@ Choose the client setup in the GUI:
- **Code OSS from sources:** apply `product.overrides.json`, reload, sign in, and
run **Developer: Sync Account Policy**.
- **Stable, Insiders, CLI, or other clients:** configure the displayed system
proxy mapping.
proxy mapping and enable Proxyman's platform proxy toggle (**Tools > macOS
Proxy** or **Tools > Override Windows Proxy**). VS Code clients must also add
the displayed `http.proxy` property to `settings.json`.
Use **Clear Policy Cache** when the runtime's fresh managed-settings cache
prevents a network request. The live request log confirms whether the client
reached the server.
Use **Clear SDK Policy Cache**, expand the macOS or Windows section, and run the
copied command when the runtime's fresh managed-settings cache prevents a network
request. Select a known policy endpoint in the live request log to open its
response editor.
To test `forceRemoteSettingsRefresh` fail-closed behavior, apply the
`customization-lockdown` managed-settings preset and sync once successfully.
Then select the `server-error` preset or choose the `malformed-json`,
`disconnect`, or `timeout` response behavior and sync again. The successful
first response seeds the cached refresh requirement; the second response
exercises HTTP, parse, immediate-network, or client-timeout failure without
manually editing payloads.
Other Copilot clients share the default cache. For deterministic testing, start
both Code OSS and the mock server with the same isolated `COPILOT_CACHE_HOME`.
+106 -13
View File
@@ -1,27 +1,120 @@
---
name: update-screenshots
description: Download screenshot baselines from the latest CI run and commit them. Use when asked to update, accept, or refresh component screenshot baselines from CI, or after the screenshot-test GitHub Action reports differences. This skill should be run as a subagent.
description: Update the committed blocks-ci screenshot hashes after the "Screenshots & Tests" check fails, or investigate a screenshot diff reported on a PR. Use when asked to update, accept, or refresh component screenshot baselines from CI. This skill should be run as a subagent.
---
# Update Component Screenshots from CI
Screenshot baselines are **no longer stored in the repository**. They are managed by an external screenshot service (`hediet-screenshots.azurewebsites.net`). The CI workflow uploads screenshots to this service and diffs them automatically.
Screenshot **images** are not stored in the repository — they live in an external service
(`hediet-screenshots.azurewebsites.net`), keyed by commit SHA. But a subset of fixtures is
pinned by **hash** in [`test/componentFixtures/blocks-ci-screenshots.md`](../../../test/componentFixtures/blocks-ci-screenshots.md),
and that file **is** committed. When those hashes change, CI fails and you must update the file.
When the `Checking Component Screenshots` GitHub Action detects changes, it posts a PR comment with before/after comparisons. No manual baseline updates are needed — the screenshots on the `main` branch commit become the new baselines automatically after merge.
## Two different outcomes, only one of which blocks
## What Changed
The `Screenshots & Tests` job in [`.github/workflows/component-fixtures.yml`](../../workflows/component-fixtures.yml)
produces two independent results:
- Baseline images were removed from `test/componentFixtures/.screenshots/baseline/`.
- Git LFS is no longer used for screenshot storage.
- The screenshot service stores images keyed by commit SHA and handles diffing.
| Result | Blocking? | Action |
| --- | --- | --- |
| Screenshot **diff report** (PR comment with before/after images) | No — informational | Review the visuals. Nothing to commit. |
| **blocks-ci hash mismatch** | **Yes — fails the check** | Update `blocks-ci-screenshots.md` and commit. |
## If Screenshots Need Investigation
A fixture opts into the blocking gate with `labels: { kind: 'screenshot', blocksCi: true }`.
Only those fixtures appear in `blocks-ci-screenshots.md`.
1. Check the PR comment posted by the CI workflow for visual diffs.
2. Download the `screenshots` artifact from the CI run for the raw captured images:
The failure looks like this:
```bash
gh run download <run-id> --name screenshots --dir .tmp/screenshots
```
##[error]blocks-ci screenshot hashes do not match committed file. See PR comment or job summary for the updated content.
```
3. Compare locally if needed. The artifact contains the full set of captured screenshots.
## Step 1: Get the expected hashes from CI
> **Never regenerate the hashes locally.** They are hashes of the rendered PNG bytes, produced
> on `ubuntu-latest`. Rendering on macOS or Windows yields different bytes and therefore
> different hashes, so locally generated values will fail CI. Always copy the values from the
> CI job.
Three surfaces carry the same content — use whichever is handy:
- The **PR comment** titled "blocks-ci screenshots changed" (non-fork PRs only) — contains the
full updated file plus a patch.
- The **job summary**, which gets the identical body and is the only surface fork PRs receive.
- The **job log**, whose final step prints a unified diff:
```bash
gh api repos/microsoft/vscode/actions/jobs/<JOB_ID>/logs > "$TMPDIR/ci-job-log.txt"
grep -n '##\[error\]' "$TMPDIR/ci-job-log.txt"
```
Find the failed job id with:
```bash
gh pr checks <PR> --json name,link,bucket --jq '.[] | select(.name == "Screenshots & Tests")'
```
## Step 2: Verify the change is intentional before accepting it
This gate exists to catch **unintended** layout regressions, so accepting new hashes without
looking at the images defeats its purpose. The images are publicly fetchable by hash, so pull
both the old (committed) and new (from CI) versions and compare:
```bash
curl -sL -o old.png "https://hediet-screenshots.azurewebsites.net/images/<OLD_HASH>"
curl -sL -o new.png "https://hediet-screenshots.azurewebsites.net/images/<NEW_HASH>"
```
Then view them, and localize the change rather than eyeballing full screenshots — the delta is
often only a pixel or two:
```bash
python3 -c "
from PIL import Image, ImageChops
a = Image.open('old.png').convert('RGB'); b = Image.open('new.png').convert('RGB')
print('diff bbox:', ImageChops.difference(a, b).getbbox())
"
```
Confirm the delta matches what the PR intends. If the fixture is unrelated to the change, or
the shift is larger than expected, treat it as a regression and fix the code instead of the
hashes.
## Step 3: Apply and commit
Edit only the changed lines in `test/componentFixtures/blocks-ci-screenshots.md`, replacing the
old hash in the image URL with the new one:
```md
#### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark
![screenshot](https://hediet-screenshots.azurewebsites.net/images/<NEW_HASH>)
```
The file is generated by [`build/lib/screenshotBlocksCi.ts`](../../../build/lib/screenshotBlocksCi.ts)
and compared **byte-for-byte**, so keep the `<!-- auto-generated by CI — do not edit manually -->`
header, the `#### <fixtureId>` / image-link pairing, the blank line between entries, and the
`fixtureId` sort order intact. Verify your edit is the exact inverse of the diff CI reported:
```bash
git diff test/componentFixtures/blocks-ci-screenshots.md
```
Then commit and push. The check re-runs and should pass; hashes on `main` become the new
baseline after merge.
## Investigating further
Raw captured images and the manifest for a run are uploaded as an artifact:
```bash
gh run download <RUN_ID> --name screenshots --dir .tmp/screenshots
```
`manifest.json` maps each `fixtureId` to its `imageHash` and any render errors.
## Related failures from the same job
The check also fails if a fixture **failed to render** (`Fail if fixtures had errors`) or if the
Playwright fixture tests failed. Those are genuine bugs — updating hashes will not help. Look
for `::error::<fixtureId>:` in the log, and download the `playwright-test-results` artifact for
test failures.
+8 -22
View File
@@ -263,8 +263,8 @@ For `IconLabel` and list/tree renderers, this is handled automatically. For cust
## 10. Design-System Size Tokens (spacing, radius, font, codicon, stroke)
VS Code ships a design-system **size** ramp, registered in
`src/vs/platform/theme/common/sizes/baseSizes.ts` (agents font ramp in
`src/vs/sessions/common/sizes.ts`) and emitted as `--vscode-*` CSS variables.
`src/vs/platform/theme/common/sizes/baseSizes.ts` and emitted as `--vscode-*` CSS
variables.
When writing or editing CSS, prefer the token var over a raw px value wherever a
token exists. The full tables + rationale live in the auto-injected
`.github/instructions/design-tokens.instructions.md` (canonical source — keep
@@ -312,8 +312,8 @@ scale value, **ties round up** (`5px → 6px`, `3px → 4px`, `1px → 2px`,
### Font size & weight
Generic UI ramp — pair a **size** token with a **weight** token (mirrors the
agents ramp; "Strong" = matching size token + `semiBold`, never a separate size):
Generic UI ramp — pair a **size** token with a **weight** token ("Strong" =
matching size token + `semiBold`, never a separate size):
| px | Size var | Weight |
|----|----------|--------|
@@ -333,28 +333,14 @@ Generic weights: `--vscode-fontWeight-regular` (400),
`--vscode-bodyFontSize-small` (12) → `--vscode-fontSize-label1`,
`--vscode-bodyFontSize-xSmall` (11) → `--vscode-fontSize-body2`.
Agents window (`src/vs/sessions/**`) ramp — identical values, `agents-`-prefixed:
| px | Size var | Weight |
|----|----------|--------|
| 26 | `--vscode-agents-fontSize-heading1` | semiBold |
| 18 | `--vscode-agents-fontSize-heading2` | semiBold |
| 13 | `--vscode-agents-fontSize-heading3` | semiBold |
| 13 | `--vscode-agents-fontSize-body1` | regular |
| 11 | `--vscode-agents-fontSize-body2` | regular |
| 12 | `--vscode-agents-fontSize-label1` | regular |
| 11 | `--vscode-agents-fontSize-label2` | regular |
| 10 | `--vscode-agents-fontSize-label3` | regular |
Both weight ramps are **two weights only**: `regular` (400) and
`semiBold` (600) — generic `--vscode-fontWeight-*`, agents
`--vscode-agents-fontWeight-*`.
The legacy Agents-specific `--vscode-agents-fontSize-*` and
`--vscode-agents-fontWeight-*` tokens are also deprecated; use the matching
generic tokens.
- **No medium (500).** `font-weight: 500` is off the ramp — snap to `semiBold`.
Likewise `700`/`bold` → round to the nearer of 400/600.
- **"Strong" is not a separate size.** "Body 1 Strong" = the matching
`--vscode-fontSize-*` (or `--vscode-agents-fontSize-*`) size token + `semiBold`.
Never add a strong *size*.
`--vscode-fontSize-*` size token + `semiBold`. Never add a strong *size*.
- `normal` ≡ 400 → `regular`. Leave `inherit`, `lighter`, `bolder`,
`var()`/`calc()` untouched.
+2 -2
View File
@@ -134,8 +134,8 @@ Reviewers will always flag hardcoded colors, shadows, sizes that should use them
| `border: 1px solid …` (width) | `var(--vscode-strokeThickness)` for the 1px width |
| `border-radius: 6px` | `var(--vscode-cornerRadius-medium)` (radius ramp) |
| `padding: 8px 12px` (off-scale) | spacing ramp (`--vscode-spacing-size*`) |
| `font-size: 14px` (arbitrary) | size ramp (`--vscode-fontSize-*`, agents `--vscode-agents-fontSize-*`) |
| `font-weight: 500` | `--vscode-fontWeight-semiBold` (agents `--vscode-agents-fontWeight-semiBold`; no 500) |
| `font-size: 14px` (arbitrary) | size ramp (`--vscode-fontSize-*`) |
| `font-weight: 500` | `--vscode-fontWeight-semiBold` (no 500) |
| codicon `font-size: 14px` | `--vscode-codiconFontSize` (16) / `-compact` (12) |
**Rule:** If a value relates to color, shadow, or border — it must come from a CSS variable or registered color token. The only exception is `0` (zero) values and purely structural measurements like `100%`.
+3
View File
@@ -61,6 +61,9 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare Electron types
run: node build/npm/electronTypes.ts
- name: Type check /build/ scripts
run: npm run typecheck
working-directory: build
+2 -2
View File
@@ -2,7 +2,8 @@
.cache
npm-debug.log
Thumbs.db
node_modules/
# No trailing slash, so a symlinked node_modules is ignored too
node_modules
.tmp/
.build/
/extensionsCG/
@@ -32,7 +33,6 @@ product.overrides.json
vscode-telemetry-docs/
test-output.json
test/componentFixtures/.screenshots/*
!test/componentFixtures/.screenshots/baseline/
dist
.playwright-cli
.playwright-mcp
+2 -2
View File
@@ -1,6 +1,6 @@
disturl="https://electronjs.org/headers"
target="42.9.3"
ms_build_id="15072006"
target="42.8.1"
ms_build_id="14906494"
runtime="electron"
ignore-scripts=false
build_from_source="true"
@@ -201,6 +201,9 @@ jobs:
- template: ../common/install-builtin-extensions.yml@self
- script: node build/npm/electronTypes.ts
displayName: Prepare Electron types
- template: ../common/agent-sdk-produce.yml@self
parameters:
vscodePlatform: alpine
@@ -15,7 +15,6 @@ shasum.update(fs.readFileSync(path.join(ROOT, 'build/.cachesalt')));
shasum.update(fs.readFileSync(path.join(ROOT, '.npmrc')));
shasum.update(fs.readFileSync(path.join(ROOT, 'build', '.npmrc')));
shasum.update(fs.readFileSync(path.join(ROOT, 'remote', '.npmrc')));
shasum.update(fs.readFileSync(path.join(import.meta.dirname, 'listNodeModules.ts')));
// Add `package.json` and `package-lock.json` files
for (const dir of dirs) {
@@ -5,7 +5,6 @@
import fs from 'fs';
import path from 'path';
import { ensureElectronTypes } from '../../npm/electronTypes.ts';
if (process.argv.length !== 3) {
console.error('Usage: node listNodeModules.ts OUTPUT_FILE');
@@ -41,7 +40,5 @@ function findNodeModulesFiles(location: string, inNodeModules: boolean, result:
}
const result: string[] = [];
await ensureElectronTypes();
findNodeModulesFiles('', false, result);
result.push('.build/typings/electron.d.ts');
fs.writeFileSync(process.argv[2], result.join('\n') + '\n');
@@ -0,0 +1,14 @@
# Keep this immediately after checkout: the GitHub credential expires after one hour, and later steps must not restore the LFS pointer files.
steps:
- script: git lfs install --local
displayName: Initialize Git LFS
- script: git lfs pull --include="extensions/copilot/test/simulation/cache/**"
condition: and(succeeded(), eq(variables['Build.Repository.Provider'], 'GitHub'))
displayName: Pull Copilot test cache from GitHub
- script: git --config-env=http.extraheader=GIT_AUTH_HEADER lfs pull --include="extensions/copilot/test/simulation/cache/**"
condition: and(succeeded(), eq(variables['Build.Repository.Provider'], 'TfsGit'))
displayName: Pull Copilot test cache from Azure Repos
env:
GIT_AUTH_HEADER: "AUTHORIZATION: bearer $(System.AccessToken)"
@@ -3,19 +3,6 @@ parameters:
type: string # linux, darwin, win32
steps:
- script: git lfs install --local
displayName: Initialize Git LFS
- script: git lfs pull --include="extensions/copilot/test/simulation/cache/**"
condition: and(succeeded(), eq(variables['Build.Repository.Provider'], 'GitHub'))
displayName: Pull Copilot test cache from GitHub
- script: git --config-env=http.extraheader=GIT_AUTH_HEADER lfs pull --include="extensions/copilot/test/simulation/cache/**"
condition: and(succeeded(), eq(variables['Build.Repository.Provider'], 'TfsGit'))
displayName: Pull Copilot test cache from Azure Repos
env:
GIT_AUTH_HEADER: "AUTHORIZATION: bearer $(System.AccessToken)"
# Setup copilot test environment (tokens, env vars)
- task: AzureCLI@2
inputs:
@@ -19,6 +19,9 @@ parameters:
steps:
- template: ../../common/checkout.yml@self
- ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}:
- template: ../../copilot/pull-test-cache.yml@self
- task: NodeTool@0
inputs:
versionSource: fromFile
@@ -158,6 +161,9 @@ steps:
- template: ../../common/install-builtin-extensions.yml@self
- script: node build/npm/electronTypes.ts
displayName: Prepare Electron types
- template: ../../common/agent-sdk-produce.yml@self
parameters:
vscodePlatform: darwin
@@ -27,6 +27,9 @@ parameters:
steps:
- template: ../../common/checkout.yml@self
- ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}:
- template: ../../copilot/pull-test-cache.yml@self
- task: NodeTool@0
inputs:
versionSource: fromFile
@@ -205,6 +208,9 @@ steps:
- template: ../../common/install-builtin-extensions.yml@self
- script: node build/npm/electronTypes.ts
displayName: Prepare Electron types
- ${{ if ne(parameters.VSCODE_ARCH, 'armhf') }}:
- template: ../../common/agent-sdk-produce.yml@self
parameters:
@@ -131,6 +131,9 @@ jobs:
- script: node build/azure-pipelines/distro/mixin-quality.ts
displayName: Mixin distro quality
- script: node build/npm/electronTypes.ts
displayName: Prepare Electron types
- script: node build/azure-pipelines/common/checkDistroCommit.ts
displayName: Check distro commit
env:
@@ -121,6 +121,9 @@ jobs:
- template: ../common/install-builtin-extensions.yml@self
- script: node build/npm/electronTypes.ts
displayName: Prepare Electron types
- script: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadCopilotVsix.ts
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
@@ -110,6 +110,9 @@ steps:
- template: ../common/install-builtin-extensions.yml@self
- script: node build/npm/electronTypes.ts
displayName: Prepare Electron types
- template: ../common/mixin-vscode-capi.yml@self
- powershell: npm run gulp core-ci
@@ -21,6 +21,9 @@ parameters:
steps:
- template: ../../common/checkout.yml@self
- ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}:
- template: ../../copilot/pull-test-cache.yml@self
- task: NodeTool@0
inputs:
versionSource: fromFile
@@ -147,6 +150,9 @@ steps:
- template: ../../common/install-builtin-extensions.yml@self
- powershell: node build/npm/electronTypes.ts
displayName: Prepare Electron types
- template: ../../common/agent-sdk-produce.yml@self
parameters:
vscodePlatform: win32
+75 -75
View File
@@ -1,75 +1,75 @@
68923728c4a777c64a7f4ea90f950c9859d981be341de75e8ee67265ebba28c9 *chromedriver-v42.9.3-darwin-arm64.zip
d37ebde4e474fbb22675c3edab45a5d6e41821c6a3050346401bfa29df9aebf0 *chromedriver-v42.9.3-darwin-x64.zip
fdd4e11784b695bd0152b0aefb1262539ac763ec684cf8a3c50bb20ebcd2b084 *chromedriver-v42.9.3-linux-arm64.zip
3cba937b4ccbe6d0409ed3064c1e126043e96160ac81183140eddfbb69366a07 *chromedriver-v42.9.3-linux-armv7l.zip
9abaadfe6c446613d3623ffb5de743aefba1afe4f8cb5928f293b330b0a7f2e6 *chromedriver-v42.9.3-linux-x64.zip
fbf7200393d1132ab33801c337ad84c97cfd1bc3f8eb316f4e228d277735f6c0 *chromedriver-v42.9.3-mas-arm64.zip
3c33090b8dba0da89482bdb89f4ec212dc51e568b0ecd1df1968f014979d5df7 *chromedriver-v42.9.3-mas-x64.zip
c6afabced225a671cbfaeed70244619e727dd2ac7e72d251e5200e04ee595a1f *chromedriver-v42.9.3-win32-arm64.zip
f90353cd0be40d37081cfacefab35be2c739401fa8b48c6b08163ffe16e9444e *chromedriver-v42.9.3-win32-ia32.zip
9aa6d1c77f6acf1fe3e3fff766bfb248cad9f5f1444a2f45ac27e61b08b4a530 *chromedriver-v42.9.3-win32-x64.zip
5ffc9fcbef859b03c171410778d50196ec23d4f28a77da1d261df9f94dbf8980 *electron-api.json
06664f3ca19752095e58e571ed1ec24d715b563f45557a3e66c812e8c5c8bd45 *electron-v42.9.3-darwin-arm64-dsym-snapshot.zip
8023590fe6e3e33372ae119e8af9dfd5936db65fcb8211aa4ba148a4e361e1bd *electron-v42.9.3-darwin-arm64-dsym.tar.xz
4f48a4b288ef52b2b2e8b3b3ffb5d0086ea385090af9210daff48ec64416e84a *electron-v42.9.3-darwin-arm64-symbols.zip
ea14213b15708bf571f4685772fb562a6469bcb9c49f4148077727d681e82e57 *electron-v42.9.3-darwin-arm64.zip
694c6e8d4bbcd6b0ed523adb3a02d1a9407782821b644969aff1141c33e82da4 *electron-v42.9.3-darwin-x64-dsym-snapshot.zip
09eb440fbb4de32e049336a86d3ce8b512e204a41e3319527e0d2c16ab47c088 *electron-v42.9.3-darwin-x64-dsym.tar.xz
414b384187615da339ca6d98074deca6a79cf0e6cee39e40efaf2333c67737ab *electron-v42.9.3-darwin-x64-symbols.zip
190b7e40410a0e00c4c9804a72e0e10491ba93c98a2a803023dd7af779cbc784 *electron-v42.9.3-darwin-x64.zip
1c9b92b2f8b20ff26e70a9a4cb811ccfc779b90243a8789d52d76cc8fe8485e8 *electron-v42.9.3-linux-arm64-debug.zip
17f1b7f074a9655d9b2f9671bd92d1594a7c7ebce1571330af46913d31c1360a *electron-v42.9.3-linux-arm64-symbols.zip
1064e5cc5aa6490bb094b5e665cad4c8d520dcd4d52581ad68877793199fb903 *electron-v42.9.3-linux-arm64.zip
09c8774f3a9813cab835398683140ece0a91966d162670f52f58c1c174d3474d *electron-v42.9.3-linux-armv7l-debug.zip
0891a775d8531d3fb89d6e76578d23c62460c9051d989df1beed19934be4eaf3 *electron-v42.9.3-linux-armv7l-symbols.zip
41439e99891463e9bca4799e13ad636d8ecc81afc05bb6ac616b12747ab01bb7 *electron-v42.9.3-linux-armv7l.zip
79e21b3ab1e809a13ed591b489f1717460f95eb21ee46a2db101d595b7dc32ce *electron-v42.9.3-linux-x64-debug.zip
6d020427efd736d3641a0beacd7e7b7e1463245585e7f5a6c06ce5426dcebf23 *electron-v42.9.3-linux-x64-symbols.zip
46fc1cd5d70de57c372fbc0f36870c4c4d80b127a0d452d80bd577c5a7d39b7d *electron-v42.9.3-linux-x64.zip
9dd624568fdc716e25a7474f96ec9c8cebca46634ee74767869978356fcadecf *electron-v42.9.3-mas-arm64-dsym-snapshot.zip
f2c25c8b918f1a4991d4d3f76e17a00077582828a08981f0880d26a1b9a842a3 *electron-v42.9.3-mas-arm64-dsym.tar.xz
da16802b0d3a0fd8cb9876d0e9115b54dae4501ddd4bcbdc64657a8c4b0070d4 *electron-v42.9.3-mas-arm64-symbols.zip
2543d991e43c84ab30d8fe05632d06589fd067cac62ae334251d0aa69ad072ad *electron-v42.9.3-mas-arm64.zip
7051fa36734634b73ee1bb5810463b48fc3a7fda1e848fcae08b5a7ad130f9ef *electron-v42.9.3-mas-x64-dsym-snapshot.zip
9572c03cfc635e4125e6a8609dca3991dc7fd14e047a8c15ca3e682df54c97da *electron-v42.9.3-mas-x64-dsym.tar.xz
224deeea2e03135b4ea55b505d7ee7362fbd88f518fc1a80ca5883a5e157c360 *electron-v42.9.3-mas-x64-symbols.zip
1d28453d4ac845bf08b9a5918f5f176d43805feba17f9a63b4213623f198c798 *electron-v42.9.3-mas-x64.zip
bfcacb8ab81126cefe9a853202aa171538a08676b6c4777ff645ae3291e6d2a9 *electron-v42.9.3-win32-arm64-pdb.zip
b08e85f1eb0348e2ef7a3bc8beba26cbd72bfaa1bb71c870c74defb729072601 *electron-v42.9.3-win32-arm64-symbols.zip
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v42.9.3-win32-arm64-toolchain-profile.zip
9871b4292ec595868d91d32caa5ad03437a919ce54e092079d7bbfa881c33975 *electron-v42.9.3-win32-arm64.zip
c1a15da9e765894baa23fe00d62ddfa502a7fd4ca532a9f75565b6268d8dfd61 *electron-v42.9.3-win32-ia32-pdb.zip
b926e911a564e8a9865458600365e51bdf14ef941147ed28030dee4cfc6f9564 *electron-v42.9.3-win32-ia32-symbols.zip
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v42.9.3-win32-ia32-toolchain-profile.zip
f1916df4930e56c416f2774ef1bcaf4dbba4a248159f384c335abfa6bcfa5e00 *electron-v42.9.3-win32-ia32.zip
20c556fa85bc087d606b4fe66f873323e8cf36effa6466ff93a1579b9a0366c2 *electron-v42.9.3-win32-x64-pdb.zip
aac8e4ce5cbb7d4f558cd6a0fd8c68863743b6518d35888819d774ae30834d87 *electron-v42.9.3-win32-x64-symbols.zip
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v42.9.3-win32-x64-toolchain-profile.zip
51b68cd32c09b6de4f468a8c2a19dddf765d450e3af13a32e790c7a7fb4aeddf *electron-v42.9.3-win32-x64.zip
63bf27ede5619690277f0e67419df9642381e9a254ce30b184432b75f1b98d97 *electron.d.ts
edc4810eb7fced0a9c10a3a9b8c5b5da0698297f82f58a9da8578c0765f97202 *ffmpeg-v42.9.3-darwin-arm64.zip
5d17c1d3d104c8707e86ba7788d34a6e54fc2a1e37bb790b6890610c161491f6 *ffmpeg-v42.9.3-darwin-x64.zip
2a2268fbd3c87237169671df43f481fb61becb224899c6639cdf57dd53936ac9 *ffmpeg-v42.9.3-linux-arm64.zip
2e24581796500f7d6ed0c16076d74b7fe0b7cf625844623535077df03886bc26 *ffmpeg-v42.9.3-linux-armv7l.zip
f58186cb2bf428629c481583c0be664355d8110aa7350d7b80d936c5eef94a99 *ffmpeg-v42.9.3-linux-x64.zip
925afdd20547657e308517b04574a62e5bf9285ca64ca47f9ed87a1b9b9fdb01 *ffmpeg-v42.9.3-mas-arm64.zip
fe2d15c601d28d1775adfe97566d8872adcb242dbb2982a2e10d625c6ea5b17a *ffmpeg-v42.9.3-mas-x64.zip
f44b0b8fd6f0b46bbea8198bd9e12054064575df7dc368057b665a586d3a440d *ffmpeg-v42.9.3-win32-arm64.zip
6a86af10627c816d072e8542ef510dae7de2cffbb411b2afc821c15bea457c33 *ffmpeg-v42.9.3-win32-ia32.zip
3bbe38c2b4853606ed91889fe1ec64c73f66f28475177d3471acab0feaeb5dec *ffmpeg-v42.9.3-win32-x64.zip
a2d95ff55f536a500e51db605de1570b5f36546d8a1ebf1fdca8d21c725a6e23 *hunspell_dictionaries.zip
4bdb4fd0982e914f074f1d3051a4cb3f036c23fbb0c480ace567ee04a27816de *libcxx-objects-v42.9.3-linux-arm64.zip
c19225c9c28ed8bb80ac30effa645069f20a579f0a2f566197e9db98a96d8e7b *libcxx-objects-v42.9.3-linux-armv7l.zip
02d50fccace6bd921ee991b58dd6d4ed6bbd3f8947ca58217b4c5e93cca4a2be *libcxx-objects-v42.9.3-linux-x64.zip
7c2c61dc6ae68fe6ec287111d01f003ff5df92da144aa67ea8e26c259c94bc1c *libcxx_headers.zip
f189cd54b7428706c11090fee3f1a4335f355790728cdf38995b36af119641b8 *libcxxabi_headers.zip
dd26b53603963dd476511092ae60e4b4e13a13192bb2888b51bdfd0b5dbbdea5 *mksnapshot-v42.9.3-darwin-arm64.zip
f8bf37b012456c1979444ed07bb7d6ca151da8f23cb97d58c98c76913666b3c1 *mksnapshot-v42.9.3-darwin-x64.zip
cbf39cfc3e67f9b6e2f8eff8039584c8e90c6052fc2ab3f265003ad9783d0c27 *mksnapshot-v42.9.3-linux-arm64-x64.zip
eece8a15de398a53dc75582a551a00a6c9212e6772a6919ec5632fe5482240e4 *mksnapshot-v42.9.3-linux-armv7l-x64.zip
78e241c8b4e1a6bc89c677b1d8c910df8e9fae76c03d42be9d90d9cf771b2a42 *mksnapshot-v42.9.3-linux-x64.zip
42b7bdc9a008b6f5ada62aa24ee4adb9a1d044d4a177e509ee64971228b744cb *mksnapshot-v42.9.3-mas-arm64.zip
1f98b77636cc76cfc4d4fc25d077d57e2fcb9b481a1d2888271177aa5a81e9e3 *mksnapshot-v42.9.3-mas-x64.zip
d87bf0c3f40ab1f7874cb326095ec809e16f5fe9bed3837958b2d7aa55fbe407 *mksnapshot-v42.9.3-win32-arm64-x64.zip
354604f84b653a944a07022e59a48e3a61066a6e6aab9f76d68fc271e11a744c *mksnapshot-v42.9.3-win32-ia32.zip
0e51db82ae7282f7e9a9e903e68cb50a1bc364676bd971955d7cab8a451aba1a *mksnapshot-v42.9.3-win32-x64.zip
1b35c5d29a11b097ff2f61b45d1cacfb79ccdff2ecd22922776a4e6459c7ecfb *chromedriver-v42.8.1-darwin-arm64.zip
a27788a398a7135b9cb2b0b56f5ba0b50e8aab1b54d83b1b413b3e1e3c770709 *chromedriver-v42.8.1-darwin-x64.zip
e7aad0ffb9a206152362d4209e2078c165814c5c91de44ad410c52385f0a8fb7 *chromedriver-v42.8.1-linux-arm64.zip
e1c4a6c39e8e9380da7eabc029447fa0a9ac8834f9b3ff6a75a1b3f8b31c1fb1 *chromedriver-v42.8.1-linux-armv7l.zip
f8839cedadeae394dda47306a35ac156d15f6d0612af01c9d04ddfd51a5537c5 *chromedriver-v42.8.1-linux-x64.zip
87b9e946001c0950589d5848b433e2052f00f4e2a0be8682fa4980bce3a480fb *chromedriver-v42.8.1-mas-arm64.zip
41de1fe6819e429c4afa696f3679d26b432bbd59aca4535955190c3bbf10104c *chromedriver-v42.8.1-mas-x64.zip
7f0e84e0f567d098f1467c18455d6c00f9698bfa8007cc845494e52da60a7054 *chromedriver-v42.8.1-win32-arm64.zip
fa25f5586d98188cc9022864babd50ca4110f1cef8ed616d56c7a6a4f39d1ec3 *chromedriver-v42.8.1-win32-ia32.zip
48a5e475df33be4e7c79f08ea9fe887a68ccaae6fd3187d09b3e0fae78c86aa4 *chromedriver-v42.8.1-win32-x64.zip
2052a8e72ff894b62851f3d23bcc4ce8683f21194b0dc973711237ee33175bed *electron-api.json
fe2be77c97b1d9adca681ddaa3f41ea895bc7fc5c3f904a0c8bc835925c430e7 *electron-v42.8.1-darwin-arm64-dsym-snapshot.zip
196bb6cc8dfeb27ddf4cece18d5c8b9aa951488135db81e0c70e179655b60d89 *electron-v42.8.1-darwin-arm64-dsym.tar.xz
b1b08c35fa7f7f4f1422f35ef310dd3dbc5cea16dcc50499b6ee3299179a18f8 *electron-v42.8.1-darwin-arm64-symbols.zip
f03df963463d120a35a194e0c172f15c611ea81dceda09a6ea275843611231fd *electron-v42.8.1-darwin-arm64.zip
4d288d7706f6f377b318498a1c2f3175926a452c2885330b6cf1aee19f95ae04 *electron-v42.8.1-darwin-x64-dsym-snapshot.zip
b23dbbca88a280ae1159759d2e00b45a4d0acac80b27a98e77dfcf4bd3176f44 *electron-v42.8.1-darwin-x64-dsym.tar.xz
a0003d15b543fa75f0a216daa3b79316211bfa6e910b9733e8a3978a2c6e2b9a *electron-v42.8.1-darwin-x64-symbols.zip
a9cb0bc4e7e41e047798c5366a2136ece4949fb9092c59d54597d6ce09bb2e09 *electron-v42.8.1-darwin-x64.zip
c0548d7fa5f182d9f41c3134a7c1860b0a133e6d410d9ce0eaa5dc9698a0cf80 *electron-v42.8.1-linux-arm64-debug.zip
5e359d0b7b1be96a6b92e93b53267fb26732a93256de8fa6ea2fbee2b8d8ba10 *electron-v42.8.1-linux-arm64-symbols.zip
072e2441ee95b9ecf0d5ae56ef4715ee949258d10ca11ae5e856d7f93a7ebb36 *electron-v42.8.1-linux-arm64.zip
d54bb6956812451ac4d6156462085e66e81a67616b64b6aa7743de355fd3eae3 *electron-v42.8.1-linux-armv7l-debug.zip
32b6a377322b5334dd5fffbd9f8cda90794648b15c7eb3c4a83be81b3f3c7857 *electron-v42.8.1-linux-armv7l-symbols.zip
8f14bbbd9717a00749928ca009a3b9ee2b4b05e6f4dbf49d1d48365dad346053 *electron-v42.8.1-linux-armv7l.zip
bffc45ef137fc592ac75b107b00ec78b6a8ca8fe4271092c2d0b37b2def43d87 *electron-v42.8.1-linux-x64-debug.zip
48f6b04c50073c6faf552966e6f2f18d02cfe58eb704e1ec352ed6c5c1f35cc5 *electron-v42.8.1-linux-x64-symbols.zip
2b47299ee6927b1e6cd6c12ee655794118349a3de48a0add97e438ebeebad809 *electron-v42.8.1-linux-x64.zip
d37e2eaa1cec6e507950124ad8a08158f2cec1b133f65a0ee170b8c44d8b0ab7 *electron-v42.8.1-mas-arm64-dsym-snapshot.zip
f17d95755dd116aad40d2703e894822607519a2658222ebd2e9442f5bb006028 *electron-v42.8.1-mas-arm64-dsym.tar.xz
ef4de6549587f7c74224c56f819772a4a2653c89e42e0af5bc759575065da532 *electron-v42.8.1-mas-arm64-symbols.zip
f3a2c8920b3923fb61eea8a2493e183f9afd1750062ebdc8f7cc92b3afe99223 *electron-v42.8.1-mas-arm64.zip
d542f6206a2382eed67ec04000b74f8123006d737a2c394b14ccee2758335500 *electron-v42.8.1-mas-x64-dsym-snapshot.zip
82879f4732de2334897892825ef73ceceb2590e45ec42fa0ad66f30d68f35042 *electron-v42.8.1-mas-x64-dsym.tar.xz
8843bf7548deb599aab32c225f9c6351af0a8379865d56f15019931295c9bb86 *electron-v42.8.1-mas-x64-symbols.zip
9728d2f1bb6688f1e74041d10e8f8a506bc10b290a6d00c45b555adea6ce46c0 *electron-v42.8.1-mas-x64.zip
a2a9bbbf06d1d858556e66102330f520ea9a3129995bdfc30828540fba9647b1 *electron-v42.8.1-win32-arm64-pdb.zip
08415bee58fd0c16c54854ef8ffe11450bdb1d7c79f7c45658e898ccf0bb8977 *electron-v42.8.1-win32-arm64-symbols.zip
93e0253324919c9cd187a9864291f4452e4632747b8ecc1d9ec7e6d6afbf86e9 *electron-v42.8.1-win32-arm64-toolchain-profile.zip
03589ff4df68a5a1a7c11b6275fb77b6b35d76e296cbeb5c27eca5fd7d165bbb *electron-v42.8.1-win32-arm64.zip
fd8cd58035df78a74f3e4ba226162bca99847317e8f068b45500ce12a0b95f0d *electron-v42.8.1-win32-ia32-pdb.zip
c5e0d4058b4cef46efb0913255ecf5932f7318317a9f2ca536db3c169cc34850 *electron-v42.8.1-win32-ia32-symbols.zip
93e0253324919c9cd187a9864291f4452e4632747b8ecc1d9ec7e6d6afbf86e9 *electron-v42.8.1-win32-ia32-toolchain-profile.zip
cb3c62378215ddd2d57e1bfe5cad9bd45d4188e2ef8b732a88972e25a0d01b78 *electron-v42.8.1-win32-ia32.zip
a20b32e2d2c8c7cbd169c4bd4f904eb316f05a52d6d2438fdc630191f92b8a14 *electron-v42.8.1-win32-x64-pdb.zip
0496a2ea91e9a4b98bb4557dcbdb3f23d7a476d463846194a3fcba83b0c71d03 *electron-v42.8.1-win32-x64-symbols.zip
93e0253324919c9cd187a9864291f4452e4632747b8ecc1d9ec7e6d6afbf86e9 *electron-v42.8.1-win32-x64-toolchain-profile.zip
7a1aff619f94ead8a377d82e1f59bfd9a31a17db5b948f82fc5e60d576fe9304 *electron-v42.8.1-win32-x64.zip
382aaaf4ffae549fb2a105f274c12900f64b9611ce5b795adb4b130334a79456 *electron.d.ts
7373ae2f14806951b289c0bafd28ce6517a6ea59f978d930e8d2cb578c564ff8 *ffmpeg-v42.8.1-darwin-arm64.zip
377b073d86cf2b0dba87e4822619c681ff31cd65b145e1aa677170abf9ffcb7a *ffmpeg-v42.8.1-darwin-x64.zip
80b5eacd0a7518ec6e577adee584a89cbd51b80eeeab42afbe539f614f807f9e *ffmpeg-v42.8.1-linux-arm64.zip
2e24581796500f7d6ed0c16076d74b7fe0b7cf625844623535077df03886bc26 *ffmpeg-v42.8.1-linux-armv7l.zip
1a693053890f4f1198f8b9b6b56a3ae28608da0a5d129ee2cb15c1a119315b23 *ffmpeg-v42.8.1-linux-x64.zip
2f4b9d486fe07df24803e5f7c280bcb618ce00b04d858ec7e7346a66ca6e55ca *ffmpeg-v42.8.1-mas-arm64.zip
19f58a28cfe9bd25e058071a4c06fce84cb5ddfdf62861a566b427f7a26f1193 *ffmpeg-v42.8.1-mas-x64.zip
d4fa9af810d569b747457d05d6c58a5ff1a366ea36283d35b127d42bac78ce94 *ffmpeg-v42.8.1-win32-arm64.zip
47a55f20bce68efe447e525ff15aa5bb370bd6bdf592e272c0a51f1d00f6b783 *ffmpeg-v42.8.1-win32-ia32.zip
31d471e7992e1cd86e0f96c80d0b4d57b7bb653b9417fe4858baef0c8959c689 *ffmpeg-v42.8.1-win32-x64.zip
0b43113d2e84dfa7532133d95c9d63b1e2351f3a34c805c00c0b95bd83b3e7f5 *hunspell_dictionaries.zip
a64a62d2064056c9877544e09f378b14c1e05321aa1da1ac4786040aa2016cec *libcxx-objects-v42.8.1-linux-arm64.zip
97a1ca26bd5363bf0ab90c2895fca4271818a75e5c9870e486736ffac60ce152 *libcxx-objects-v42.8.1-linux-armv7l.zip
c954df6f47c48686fae79aee1645e591c822997c44e0801ee23ec2466d34ab04 *libcxx-objects-v42.8.1-linux-x64.zip
1922727da0c69a22f3be97dce1f024b0412c817eaeace9ea2fbffcae1575cf71 *libcxx_headers.zip
4c150f4569cb6c661f2c6def9aac93bc396520fe0dccb9544a95958e74ccfa7b *libcxxabi_headers.zip
319d46eb1d877463f4d53289800ee1cf111dff67034a344837550753a131ef4f *mksnapshot-v42.8.1-darwin-arm64.zip
ac7d64f9b38b2d4d1fe3c654663e90282b34893f7b853df5a749ca6f0d952bfd *mksnapshot-v42.8.1-darwin-x64.zip
b4329a9eceb1b7029e1396e4eefc197001dbffa4921e560d915d27c7f33340c7 *mksnapshot-v42.8.1-linux-arm64-x64.zip
71702aaed0fd48738f727d4c35bcd94798c46cac0d1cf8f87130861245b009b0 *mksnapshot-v42.8.1-linux-armv7l-x64.zip
25aecb76b33f8163b7455c6b942e48857500dac64aca69125c8f7e9d0deb8a81 *mksnapshot-v42.8.1-linux-x64.zip
4b39b9b51151f5674b116f531964623ecf3141e9f92c4839126463d2f3834d6e *mksnapshot-v42.8.1-mas-arm64.zip
4adcea8e1766b8c2228b1094692de0cd56de384449026cc0a9b26158789f2a69 *mksnapshot-v42.8.1-mas-x64.zip
471cd2a61e31f092ee71cff18a936cefe7903daa828b1c00e95463b066295059 *mksnapshot-v42.8.1-win32-arm64-x64.zip
e0811a7d72e763ae5f89dd6d91e1e17e1cc3ac9d3e3c8e8fc993c43fa304c0d7 *mksnapshot-v42.8.1-win32-ia32.zip
1c1f3909f03e589924c9aa2f311c90333f5d66d26b43ae21b42efe7d9907a47a *mksnapshot-v42.8.1-win32-x64.zip
+2
View File
@@ -573,6 +573,8 @@ function patchWin32DependenciesTask(destinationFolderName: string) {
promisify(glob)('**/*.node', { cwd }),
promisify(glob)('**/rg.exe', { cwd }),
promisify(glob)('**/tgrep.exe', { cwd }),
// TODO@anthonykim1 Remove once @github/copilot ships OneAuthInterop.dll with complete version information.
promisify(glob)('**/OneAuthInterop.dll', { cwd }),
])).flatMap(o => o);
const packageJsonContents = JSON.parse(await fs.promises.readFile(path.join(cwd, 'package.json'), 'utf8'));
const productContents = JSON.parse(await fs.promises.readFile(path.join(cwd, 'product.json'), 'utf8'));
+2
View File
@@ -648,6 +648,8 @@ function patchWin32DependenciesTask(destinationFolderName: string) {
glob('**/rg.exe', { cwd }),
glob('**/tgrep.exe', { cwd }),
glob('**/*explorer_command*.dll', { cwd }),
// TODO@anthonykim1 Remove once @github/copilot ships OneAuthInterop.dll with complete version information.
glob('**/OneAuthInterop.dll', { cwd }),
])).flatMap(o => o);
const packageJson = JSON.parse(await fs.promises.readFile(path.join(cwd, versionedResourcesFolder, 'resources', 'app', 'package.json'), 'utf8'));
const product = JSON.parse(await fs.promises.readFile(path.join(cwd, versionedResourcesFolder, 'resources', 'app', 'product.json'), 'utf8'));
+13
View File
@@ -76,6 +76,13 @@ const copilotTgrepPlatforms = [
const mxcArchitectures = ['x64', 'arm64'];
const copilotOutOfProcessRuntimeExecutables = [
'copilot-runtime',
'copilot-runtime-bin',
'copilot-runtime.exe',
'copilot-runtime-bin.exe',
];
function toCopilotTgrepPlatformArch(platform: string, arch: string): string {
if (platform === 'alpine') {
return `linuxmusl-${arch}`;
@@ -191,6 +198,7 @@ export function getCopilotExcludeFilter(platform: string, arch: string): string[
...excludes,
'!**/node_modules/@github/copilot-*/copilot',
'!**/node_modules/@github/copilot-*/copilot.exe',
...copilotOutOfProcessRuntimeExecutables.map(executable => `!**/node_modules/@github/copilot-*/prebuilds/*/${executable}`),
];
}
@@ -211,6 +219,7 @@ export function getCopilotRuntimePrebuildFiles(platform: string, arch: string, n
path.posix.join(copilotPlatformPackageDir, '**'),
`!${path.posix.join(copilotPlatformPackageDir, 'copilot')}`,
`!${path.posix.join(copilotPlatformPackageDir, 'copilot.exe')}`,
...copilotOutOfProcessRuntimeExecutables.map(executable => `!${path.posix.join(copilotPlatformPackageDir, 'prebuilds', '*', executable)}`),
...copilotOptionalNativePayloadDirs.map(dir => `!${path.posix.join(copilotPlatformPackageDir, dir, '**')}`),
...getCopilotOptionalNativePayloadFiles(platform).map(file => `!${path.posix.join(copilotPlatformPackageDir, file)}`),
];
@@ -316,6 +325,10 @@ function materializeBuiltInCopilotSdkPlatformFiles(copilotPackagePlatformArch: s
// Built-in materialization copies the whole prebuilds tree (not the gulp
// exclude globs above), so drop mediaremote-adapter explicitly afterward.
fs.rmSync(path.join(sdkPrebuildsTarget, 'mediaremote-adapter'), { recursive: true, force: true });
// The out-of-process runtime wrappers are not used by the built-in extension.
for (const executable of copilotOutOfProcessRuntimeExecutables) {
fs.rmSync(path.join(sdkPrebuildsTarget, executable), { force: true });
}
if (!copilotTgrepPlatforms.includes(tgrepPlatformArch)) {
return;
+4
View File
@@ -110,6 +110,10 @@
"name": "vs/workbench/contrib/folding",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/github",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/html",
"project": "vscode-workbench"
@@ -492,6 +492,16 @@ interface IDeprecatedToken {
/** Deprecated token var -> its replacement. */
const DEPRECATED_TOKENS: readonly IDeprecatedToken[] = [
{ deprecated: '--vscode-agents-fontSize-heading1', replacement: '--vscode-fontSize-heading1' },
{ deprecated: '--vscode-agents-fontSize-heading2', replacement: '--vscode-fontSize-heading2' },
{ deprecated: '--vscode-agents-fontSize-heading3', replacement: '--vscode-fontSize-heading3' },
{ deprecated: '--vscode-agents-fontSize-body1', replacement: '--vscode-fontSize-body1' },
{ deprecated: '--vscode-agents-fontSize-body2', replacement: '--vscode-fontSize-body2' },
{ deprecated: '--vscode-agents-fontSize-label1', replacement: '--vscode-fontSize-label1' },
{ deprecated: '--vscode-agents-fontSize-label2', replacement: '--vscode-fontSize-label2' },
{ deprecated: '--vscode-agents-fontSize-label3', replacement: '--vscode-fontSize-label3' },
{ deprecated: '--vscode-agents-fontWeight-regular', replacement: '--vscode-fontWeight-regular' },
{ deprecated: '--vscode-agents-fontWeight-semiBold', replacement: '--vscode-fontWeight-semiBold' },
{ deprecated: '--vscode-bodyFontSize', replacement: '--vscode-fontSize-body1' },
{ deprecated: '--vscode-bodyFontSize-small', replacement: '--vscode-fontSize-label1' },
{ deprecated: '--vscode-bodyFontSize-xSmall', replacement: '--vscode-fontSize-body2' },
@@ -983,6 +983,7 @@
"--activity-bar-icon-size",
"--activity-bar-width",
"--agent-sessions-editor-tab-padding",
"--session-chat-base-height",
"--editor-font-size",
"--background-dark",
"--background-light",
+44
View File
@@ -39,6 +39,10 @@ suite('copilot', () => {
'node_modules/@github/copilot-linux-x64/**',
'!node_modules/@github/copilot-linux-x64/copilot',
'!node_modules/@github/copilot-linux-x64/copilot.exe',
'!node_modules/@github/copilot-linux-x64/prebuilds/*/copilot-runtime',
'!node_modules/@github/copilot-linux-x64/prebuilds/*/copilot-runtime-bin',
'!node_modules/@github/copilot-linux-x64/prebuilds/*/copilot-runtime.exe',
'!node_modules/@github/copilot-linux-x64/prebuilds/*/copilot-runtime-bin.exe',
'!node_modules/@github/copilot-linux-x64/clipboard/**',
'!node_modules/@github/copilot-linux-x64/foundry-local-sdk/**',
'!node_modules/@github/copilot-linux-x64/mxc-bin/**',
@@ -57,6 +61,7 @@ suite('copilot', () => {
'prebuilds/linux-x64/pty.node',
]);
assertCopilotStandaloneExecutableExcluded(files, 'node_modules/@github/copilot-linux-x64');
assertCopilotOutOfProcessRuntimeExecutablesExcluded(files, 'node_modules/@github/copilot-linux-x64');
assertOptionalCopilotNativeDependenciesExcluded(files, 'node_modules/@github/copilot-linux-x64');
});
@@ -67,6 +72,10 @@ suite('copilot', () => {
'node_modules/@github/copilot-linuxmusl-x64/**',
'!node_modules/@github/copilot-linuxmusl-x64/copilot',
'!node_modules/@github/copilot-linuxmusl-x64/copilot.exe',
'!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/copilot-runtime',
'!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/copilot-runtime-bin',
'!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/copilot-runtime.exe',
'!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/copilot-runtime-bin.exe',
'!node_modules/@github/copilot-linuxmusl-x64/clipboard/**',
'!node_modules/@github/copilot-linuxmusl-x64/foundry-local-sdk/**',
'!node_modules/@github/copilot-linuxmusl-x64/mxc-bin/**',
@@ -84,6 +93,7 @@ suite('copilot', () => {
'prebuilds/linuxmusl-x64/runtime.node',
]);
assertCopilotStandaloneExecutableExcluded(files, 'node_modules/@github/copilot-linuxmusl-x64');
assertCopilotOutOfProcessRuntimeExecutablesExcluded(files, 'node_modules/@github/copilot-linuxmusl-x64');
assertOptionalCopilotNativeDependenciesExcluded(files, 'node_modules/@github/copilot-linuxmusl-x64');
});
@@ -92,6 +102,10 @@ suite('copilot', () => {
'node_modules/@github/copilot-win32-x64/**',
'!node_modules/@github/copilot-win32-x64/copilot',
'!node_modules/@github/copilot-win32-x64/copilot.exe',
'!node_modules/@github/copilot-win32-x64/prebuilds/*/copilot-runtime',
'!node_modules/@github/copilot-win32-x64/prebuilds/*/copilot-runtime-bin',
'!node_modules/@github/copilot-win32-x64/prebuilds/*/copilot-runtime.exe',
'!node_modules/@github/copilot-win32-x64/prebuilds/*/copilot-runtime-bin.exe',
'!node_modules/@github/copilot-win32-x64/clipboard/**',
'!node_modules/@github/copilot-win32-x64/foundry-local-sdk/**',
'!node_modules/@github/copilot-win32-x64/mxc-bin/**',
@@ -113,11 +127,16 @@ suite('copilot', () => {
'prebuilds/win32-x64/conpty/conpty.dll',
]);
assertCopilotStandaloneExecutableExcluded(getCopilotRuntimePrebuildFiles('win32', 'x64'), 'node_modules/@github/copilot-win32-x64');
assertCopilotOutOfProcessRuntimeExecutablesExcluded(getCopilotRuntimePrebuildFiles('win32', 'x64'), 'node_modules/@github/copilot-win32-x64');
assert.deepStrictEqual(getCopilotRuntimePrebuildFiles('win32', 'arm64'), [
'node_modules/@github/copilot-win32-arm64/**',
'!node_modules/@github/copilot-win32-arm64/copilot',
'!node_modules/@github/copilot-win32-arm64/copilot.exe',
'!node_modules/@github/copilot-win32-arm64/prebuilds/*/copilot-runtime',
'!node_modules/@github/copilot-win32-arm64/prebuilds/*/copilot-runtime-bin',
'!node_modules/@github/copilot-win32-arm64/prebuilds/*/copilot-runtime.exe',
'!node_modules/@github/copilot-win32-arm64/prebuilds/*/copilot-runtime-bin.exe',
'!node_modules/@github/copilot-win32-arm64/clipboard/**',
'!node_modules/@github/copilot-win32-arm64/foundry-local-sdk/**',
'!node_modules/@github/copilot-win32-arm64/mxc-bin/**',
@@ -130,6 +149,7 @@ suite('copilot', () => {
]);
assertOptionalCopilotNativeDependenciesExcluded(getCopilotRuntimePrebuildFiles('win32', 'x64'), 'node_modules/@github/copilot-win32-x64');
assertCopilotStandaloneExecutableExcluded(getCopilotRuntimePrebuildFiles('win32', 'arm64'), 'node_modules/@github/copilot-win32-arm64');
assertCopilotOutOfProcessRuntimeExecutablesExcluded(getCopilotRuntimePrebuildFiles('win32', 'arm64'), 'node_modules/@github/copilot-win32-arm64');
});
test('keeps macOS runtime prebuilds in the selected platform package', () => {
@@ -145,6 +165,7 @@ suite('copilot', () => {
'prebuilds/darwin-arm64/spawn-helper',
]);
assertCopilotStandaloneExecutableExcluded(files, 'node_modules/@github/copilot-darwin-arm64');
assertCopilotOutOfProcessRuntimeExecutablesExcluded(files, 'node_modules/@github/copilot-darwin-arm64');
assertOptionalCopilotNativeDependenciesExcluded(files, 'node_modules/@github/copilot-darwin-arm64');
});
@@ -186,6 +207,12 @@ suite('copilot', () => {
assert(files.includes('**'));
assert(files.includes('!**/node_modules/@github/copilot-*/copilot'));
assert(files.includes('!**/node_modules/@github/copilot-*/copilot.exe'));
assert.deepStrictEqual(files.filter(file => file.includes('/prebuilds/*/copilot-runtime')), [
'!**/node_modules/@github/copilot-*/prebuilds/*/copilot-runtime',
'!**/node_modules/@github/copilot-*/prebuilds/*/copilot-runtime-bin',
'!**/node_modules/@github/copilot-*/prebuilds/*/copilot-runtime.exe',
'!**/node_modules/@github/copilot-*/prebuilds/*/copilot-runtime-bin.exe',
]);
});
test('materializes target Copilot SDK prebuilds and tgrep for the built-in extension', () => {
@@ -203,6 +230,9 @@ suite('copilot', () => {
fs.writeFileSync(path.join(platformPackageDir, 'package.json'), JSON.stringify({ version: '1.0.73' }));
fs.mkdirSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'mediaremote-adapter', 'MediaRemoteAdapter.framework'), { recursive: true });
fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'runtime.node'), '');
fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'copilot-runtime.exe'), '');
fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'copilot-runtime-bin.exe'), '');
fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'OneAuthInterop.dll'), '');
fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty.node'), '');
fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe'), '');
fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'mediaremote-adapter', 'mediaremote-adapter.pl'), '');
@@ -215,6 +245,9 @@ suite('copilot', () => {
prepareBuiltInCopilotRipgrepShim('win32', 'x64', builtInCopilotExtensionDir, appNodeModulesDir);
assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'runtime.node')));
assert(!fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'copilot-runtime.exe')));
assert(!fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'copilot-runtime-bin.exe')));
assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'OneAuthInterop.dll')));
assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'conpty.node')));
assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe')));
assert(!fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'mediaremote-adapter')));
@@ -316,6 +349,10 @@ suite('copilot', () => {
...copilotPlatforms.map(platform => `!**/node_modules/@github/copilot-${platform}/**`),
'!**/node_modules/@github/copilot-*/copilot',
'!**/node_modules/@github/copilot-*/copilot.exe',
'!**/node_modules/@github/copilot-*/prebuilds/*/copilot-runtime',
'!**/node_modules/@github/copilot-*/prebuilds/*/copilot-runtime-bin',
'!**/node_modules/@github/copilot-*/prebuilds/*/copilot-runtime.exe',
'!**/node_modules/@github/copilot-*/prebuilds/*/copilot-runtime-bin.exe',
]
);
});
@@ -363,6 +400,13 @@ function assertCopilotStandaloneExecutableExcluded(patterns: string[], packageDi
}
}
function assertCopilotOutOfProcessRuntimeExecutablesExcluded(patterns: string[], packageDir: string): void {
for (const executable of ['copilot-runtime', 'copilot-runtime-bin', 'copilot-runtime.exe', 'copilot-runtime-bin.exe']) {
assert(patterns.includes(`!${packageDir}/prebuilds/*/${executable}`), executable);
assert(!matchesGlob(`${packageDir}/prebuilds/test-platform/${executable}`, patterns), executable);
}
}
function assertOptionalCopilotNativeDependenciesExcluded(patterns: string[], packageDir: string): void {
for (const dir of ['clipboard', 'foundry-local-sdk', 'mxc-bin', 'pvrecorder', 'webview']) {
assert(patterns.includes(`!${packageDir}/${dir}/**`), dir);
+1
View File
@@ -29,6 +29,7 @@ export const referenceGeneratedDepsByArch = {
'libatk-bridge2.0-0 (>= 2.5.3)',
'libatk1.0-0 (>= 2.11.90)',
'libatspi2.0-0 (>= 2.9.90)',
'libc6 (>= 2.15)',
'libc6 (>= 2.16)',
'libc6 (>= 2.17)',
'libc6 (>= 2.2.5)',
-2
View File
@@ -81,7 +81,6 @@ export const referenceGeneratedDepsByArch = {
'libgtk-3.so.0()(64bit)',
'libm.so.6()(64bit)',
'libm.so.6(GLIBC_2.2.5)(64bit)',
'libm.so.6(GLIBC_2.27)(64bit)',
'libnspr4.so()(64bit)',
'libnss3.so()(64bit)',
'libnss3.so(NSS_3.11)(64bit)',
@@ -276,7 +275,6 @@ export const referenceGeneratedDepsByArch = {
'libgtk-3.so.0()(64bit)',
'libm.so.6()(64bit)',
'libm.so.6(GLIBC_2.17)(64bit)',
'libm.so.6(GLIBC_2.27)(64bit)',
'libnspr4.so()(64bit)',
'libnss3.so()(64bit)',
'libnss3.so(NSS_3.11)(64bit)',
+3 -3
View File
@@ -529,13 +529,13 @@
"git": {
"name": "electron",
"repositoryUrl": "https://github.com/electron/electron",
"commitHash": "77e694f22b6a7c4233a371a8f4dfdb235a182e86",
"tag": "42.9.3"
"commitHash": "fc77625f01310b17d7e407b7c5600f9c15628ecb",
"tag": "42.8.1"
}
},
"isOnlyProductionDependency": true,
"license": "MIT",
"version": "42.9.3"
"version": "42.8.1"
},
{
"component": {
+15
View File
@@ -208,6 +208,7 @@ async fn handle(
};
append_secret_headers(&ctx.cm.base_path, &mut res, &client_key_half);
append_frame_ancestors(&mut res);
Ok(res)
}
@@ -276,6 +277,20 @@ fn append_secret_headers(
);
}
/// Prevents other origins from embedding serve-web pages. Same-origin iframes
/// used by the workbench itself are still allowed.
fn append_frame_ancestors(res: &mut Response<HyperBody>) {
let headers = res.headers_mut();
headers.append(
::http::header::CONTENT_SECURITY_POLICY,
"frame-ancestors 'self'".parse().unwrap(),
);
headers.insert(
::http::header::HeaderName::from_static("x-frame-options"),
"SAMEORIGIN".parse().unwrap(),
);
}
/// Gets the release info from the VS Code path prefix, which is in the
/// format `/<quality>-<commit>/...`
fn get_release_from_path(path: &str, platform: Platform) -> Option<(Release, String)> {
+63 -6
View File
@@ -43,6 +43,7 @@ static LISTENING_PORT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Extension host agent listening on (.+)").unwrap());
static WEB_UI_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Web UI available at (.+)").unwrap());
const AGENT_HOST_BRIDGE_CONNECTION_TOKEN_ENV: &str = "VSCODE_AGENT_HOST_BRIDGE_CONNECTION_TOKEN";
#[derive(Clone, Debug, Default)]
pub struct CodeServerArgs {
@@ -172,12 +173,18 @@ impl CodeServerArgs {
if let Some(host) = &self.agent_host_bridge_host {
args.push(format!("--agent-host-bridge-host={host}"));
}
if let Some(token) = &self.agent_host_bridge_connection_token {
args.push(format!("--agent-host-bridge-connection-token={token}"));
}
}
args
}
fn apply_to_command(&self, command: &mut Command) {
command.args(self.command_arguments());
if self.agent_host_bridge_port.is_some() {
if let Some(token) = &self.agent_host_bridge_connection_token {
command.env(AGENT_HOST_BRIDGE_CONNECTION_TOKEN_ENV, token);
}
}
}
}
/// Base server params that can be `resolve()`d to a `ResolvedServerParams`.
@@ -630,7 +637,11 @@ impl<'a> ServerBuilder<'a> {
async fn spawn_server_process(&self, mut cmd: Command) -> Result<Child, AnyError> {
info!(self.logger, "Starting server...");
debug!(self.logger, "Starting server with command... {:?}", cmd);
debug!(
self.logger,
"Starting server process: {:?}",
cmd.as_std().get_program()
);
// On Windows spawning a code-server binary will run cmd.exe /c C:\path\to\code-server.cmd...
// This spawns a cmd.exe window for the user, which if they close will kill the code-server process
@@ -688,8 +699,10 @@ impl<'a> ServerBuilder<'a> {
fn get_base_command(&self) -> Command {
let mut cmd = new_script_command(&self.server_paths.executable);
cmd.stdin(std::process::Stdio::null())
.args(self.server_params.code_server_args.command_arguments());
cmd.stdin(std::process::Stdio::null());
self.server_params
.code_server_args
.apply_to_command(&mut cmd);
cmd
}
}
@@ -959,3 +972,47 @@ async fn get_should_use_breakaway_from_job() -> bool {
cmd.args(["/C", "echo ok"]).output().await.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_host_bridge_connection_token_is_only_in_command_environment() {
let args = CodeServerArgs {
agent_host_bridge_host: Some("127.0.0.1".to_string()),
agent_host_bridge_port: Some(9000),
agent_host_bridge_connection_token: Some("secret-token".to_string()),
..Default::default()
};
let mut command = Command::new("code-server");
args.apply_to_command(&mut command);
let command = command.as_std();
assert_eq!(
(
command
.get_args()
.map(|argument| argument.to_string_lossy().into_owned())
.collect::<Vec<_>>(),
command
.get_envs()
.map(|(name, value)| (
name.to_string_lossy().into_owned(),
value.map(|value| value.to_string_lossy().into_owned())
))
.collect::<Vec<_>>(),
),
(
vec![
"--agent-host-bridge-port=9000".to_string(),
"--agent-host-bridge-host=127.0.0.1".to_string(),
],
vec![(
AGENT_HOST_BRIDGE_CONNECTION_TOKEN_ENV.to_string(),
Some("secret-token".to_string()),
)],
)
);
}
}
+32 -1
View File
@@ -527,16 +527,22 @@ fn make_socket_rpc(
handle_serve(c, params).await
});
rpc.register_async("update", |p: UpdateParams, c| async move {
ensure_auth(&c.auth_state)?;
handle_update(&c.http, &c.log, &c.did_update, &p).await
});
rpc.register_sync("servermsg", |m: ServerMessageParams, c| {
ensure_auth(&c.auth_state)?;
if let Err(e) = handle_server_message(&c.log, &c.server_bridges, m) {
warning!(c.log, "error handling call: {:?}", e);
}
Ok(EmptyObject {})
});
rpc.register_sync("prune", |_: EmptyObject, c| handle_prune(&c.launcher_paths));
rpc.register_sync("prune", |_: EmptyObject, c| {
ensure_auth(&c.auth_state)?;
handle_prune(&c.launcher_paths)
});
rpc.register_async("callserverhttp", |p: CallServerHttpParams, c| async move {
ensure_auth(&c.auth_state)?;
let code_server = c.code_server.lock().await.clone();
handle_call_server_http(code_server, p).await
});
@@ -579,6 +585,7 @@ fn make_socket_rpc(
},
);
rpc.register_sync("httpheaders", |p: HttpHeadersParams, c| {
ensure_auth(&c.auth_state)?;
if let Some(req) = c.http_requests.lock().unwrap().get(&p.req_id) {
trace!(c.log, "got {} response for req {}", p.status_code, p.req_id);
req.initial_response(p.status_code, p.headers);
@@ -588,6 +595,7 @@ fn make_socket_rpc(
Ok(EmptyObject {})
});
rpc.register_sync("httpbody", move |p: HttpBodyParams, c| {
ensure_auth(&c.auth_state)?;
let mut reqs = c.http_requests.lock().unwrap();
if let Some(req) = reqs.get(&p.req_id) {
if !p.segment.is_empty() {
@@ -1556,3 +1564,26 @@ async fn do_challenge_response_flow(
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ensure_auth_allows_only_authenticated_state() {
let waiting = Arc::new(std::sync::Mutex::new(AuthState::WaitingForChallenge(None)));
let issued = Arc::new(std::sync::Mutex::new(AuthState::ChallengeIssued(
"challenge".into(),
)));
let authed = Arc::new(std::sync::Mutex::new(AuthState::Authenticated));
assert_eq!(
[
ensure_auth(&waiting).is_ok(),
ensure_auth(&issued).is_ok(),
ensure_auth(&authed).is_ok(),
],
[false, false, true]
);
}
}
@@ -141,6 +141,10 @@ export class MockChatSessionMetadataStore implements IChatSessionMetadataStore {
this._sessionOrigins.set(sessionId, 'vscode');
}
setSessionOriginForTest(sessionId: string, origin: 'vscode' | 'other'): void {
this._sessionOrigins.set(sessionId, origin);
}
async getSessionOrigin(sessionId: string): Promise<'vscode' | 'other'> {
return this._sessionOrigins.get(sessionId) ?? 'vscode';
}
@@ -321,6 +321,8 @@ Orchestrates the start and end of each chat request turn, coordinating worktree
- **Steering mode**: When a session is already busy (`InProgress` or `NeedsInput`), use `send({ mode: 'immediate' })` to inject messages into the running conversation instead of starting a new request.
- **External sessions are never listed**: `isExternalSession()` in `node/copilotcliSessionService.ts` filters out every session whose `IChatSessionMetadataStore` origin is not `vscode` (e.g. started from the terminal CLI). It is applied on *all* listing paths — `shouldShowSession()` (disk), `getSessionItemImpl()` (targeted refresh), and the in-progress wrapper fallback in `_getAllSessions()` — because `getSession()` can load an external session into `_sessionWrappers` without changing its origin. There is no setting for this — the Agent Host owns external session visibility via `chat.agentSessions.showExternalAgentSessions`.
## Commands & Slash Commands
**Copilot CLI commands** (user-facing, sent programmatically):
@@ -155,6 +155,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
private readonly _sessionWorkingDirectories = new Map<string, Uri | undefined>();
private readonly _cachedSessionItems = new Map<string, ICopilotCLISessionItem>();
private readonly _newSessionIds = new Set<string>();
/** Sessions created or forked by this window. The origin metadata write is async, so treat them as local right away. */
private readonly _vscodeOriginSessionIds = new Set<string>();
/** Bridge processor that forwards SDK native OTel spans to the debug panel. */
private _bridgeProcessor: CopilotCliBridgeSpanProcessor | undefined;
/** Whether we've attempted to install the bridge (only try once). */
@@ -312,6 +314,11 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
}
public async getSessionItemImpl(sessionId: string, source: 'inMemorySession' | 'disk', token: CancellationToken): Promise<ICopilotCLISessionItem | undefined> {
// Checked up front so that a loaded external session cannot slip in via the wrapper below.
if (await this.isExternalSession(sessionId)) {
return;
}
const wrappedSession = this._sessionWrappers.get(sessionId);
// Give preference to the session we have in memory, as this contains the latest information.
if (wrappedSession && (source === 'inMemorySession' || wrappedSession.object.status === ChatSessionStatus.InProgress)) {
@@ -487,6 +494,9 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
.filter(session => !diskSessionIds.has(session.object.sessionId))
.filter(session => session.object.status === ChatSessionStatus.InProgress)
.map(async (session): Promise<ICopilotCLISessionItem | undefined> => {
if (await this.isExternalSession(session.object.sessionId)) {
return;
}
const label = session.object.title ?? await this.customSessionTitleService.getCustomSessionTitle(session.object.sessionId) ?? labelFromPrompt(session.object.pendingPrompt ?? '');
if (!label) {
return;
@@ -605,6 +615,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
session.object.add(mcpGateway);
// Set origin
this._vscodeOriginSessionIds.add(session.object.sessionId);
void this._chatSessionMetadataStore.setSessionOrigin(session.object.sessionId);
// Set session parent id
@@ -667,11 +678,27 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
}
}
/**
* Sessions created outside VS Code (e.g. started from the terminal CLI) are never surfaced by
* this provider. The Agent Host owns external session visibility via
* `chat.agentSessions.showExternalAgentSessions`.
*/
private async isExternalSession(sessionId: string): Promise<boolean> {
if (isUntitledSessionId(sessionId) || this._vscodeOriginSessionIds.has(sessionId)) {
return false;
}
return await this._chatSessionMetadataStore.getSessionOrigin(sessionId) !== 'vscode';
}
private async shouldShowSession(sessionId: string, context?: SessionContext): Promise<boolean> {
if (isUntitledSessionId(sessionId)) {
return true;
}
if (await this.isExternalSession(sessionId)) {
return false;
}
// If we're in an empty workspace then show all sessions.
if (this.workspaceService.getWorkspaceFolders().length === 0) {
return true;
@@ -1031,6 +1058,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
const { sessionId: newSessionId } = await sessionManager.forkSession(sessionId, toEventId);
const forkedTitlePrefix = l10n.t("Forked: ");
const customTitle = title.startsWith(forkedTitlePrefix) ? title : l10n.t("Forked: {0}", title);
this._vscodeOriginSessionIds.add(newSessionId);
await this._chatSessionMetadataStore.storeForkedSessionMetadata(sessionId, newSessionId, customTitle);
this._onDidChangeSessions.fire();
@@ -1160,6 +1188,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
this._sessionLabels.delete(sessionId);
this._partialSessionHistories.delete(sessionId);
this._sessionWorkingDirectories.delete(sessionId);
this._vscodeOriginSessionIds.delete(sessionId);
try {
{
const session = this._sessionWrappers.get(sessionId);
@@ -727,6 +727,35 @@ describe('CopilotCLISessionService', () => {
});
});
it('does not list sessions created outside VS Code, even once loaded into memory', async () => {
const external = new MockCliSdkSession('external-cli', new Date(0));
external.summary = 'external-cli';
manager.sessions.set(external.sessionId, external);
metadataStore.setSessionOriginForTest(external.sessionId, 'other');
const local = new MockCliSdkSession('vscode-created', new Date(0));
local.summary = 'vscode-created';
manager.sessions.set(local.sessionId, local);
const listedBeforeLoad = await service.getAllSessions(CancellationToken.None);
// Loading an external session into `_sessionWrappers` must not make it listable.
const loaded = await service.getSession({ sessionId: external.sessionId, ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None);
disposables.add(loaded!);
expect({
listedBeforeLoad: listedBeforeLoad.map(item => item.id),
listedAfterLoad: (await service.getAllSessions(CancellationToken.None)).map(item => item.id),
externalItem: await service.getSessionItem(external.sessionId, CancellationToken.None),
localItem: (await service.getSessionItem(local.sessionId, CancellationToken.None))?.id,
}).toEqual({
listedBeforeLoad: ['vscode-created'],
listedAfterLoad: ['vscode-created'],
externalItem: undefined,
localItem: 'vscode-created',
});
});
it('will not list created sessions', async () => {
const session = await service.createSession({ model: 'gpt-test', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None);
disposables.add(session);
@@ -10,10 +10,12 @@ import { IChatSessionService } from '../../../platform/chat/common/chatSessionSe
import { IInteractionService } from '../../../platform/chat/common/interactionService';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
import { isAutoExplainabilityHidden } from '../../../platform/endpoint/node/autoChatEndpoint';
import { IAutomodeService, reportAutoModeRouting } from '../../../platform/endpoint/node/automodeService';
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { ChatExtPerfMark, clearChatExtMarks, markChatExt } from '../../../util/common/performance';
import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
import { Disposable, DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
import { autorun } from '../../../util/vs/base/common/observableInternal';
import { generateUuid } from '../../../util/vs/base/common/uuid';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
@@ -69,6 +71,7 @@ class ChatAgents implements IDisposable {
@IChatQuotaService private readonly _chatQuotaService: IChatQuotaService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExperimentationService private readonly experimentationService: IExperimentationService,
@IAutomodeService private readonly automodeService: IAutomodeService,
@IPromptCategorizerService private readonly promptCategorizerService: IPromptCategorizerService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IChatSessionService chatSessionService: IChatSessionService,
@@ -204,6 +207,11 @@ Learn more about [GitHub Copilot](https://docs.github.com/copilot/using-github-c
private getChatParticipantHandler(id: string, name: string, defaultIntentIdOrGetter: IntentOrGetter): vscode.ChatExtendedRequestHandler {
return async (request, context, stream, token): Promise<vscode.ChatResult> => {
markChatExt(request.sessionId, ChatExtPerfMark.WillHandleParticipant);
// Installed before anything resolves an endpoint, since that is what
// triggers Auto's first route. Inline chat has no room for the row.
const autoRouting = request.location2 === undefined && !isAutoExplainabilityHidden(this.experimentationService)
? reportAutoModeRouting(request, stream, this.automodeService)
: Disposable.None;
try {
// If we need to switch to the base model, this function will handle it
// Otherwise it just returns the same request passed into it
@@ -269,6 +277,7 @@ Learn more about [GitHub Copilot](https://docs.github.com/copilot/using-github-c
return result;
} finally {
autoRouting.dispose();
markChatExt(request.sessionId, ChatExtPerfMark.DidHandleParticipant);
clearChatExtMarks(request.sessionId);
}
@@ -515,7 +515,7 @@ class InlineChatToolCalling {
if (result.hasError) {
failedEdits.push([toolCall, result]);
stream.progress(l10n.t('Looking not yet good, trying again...'));
stream.progress(l10n.t('An error occurred, trying again...'));
}
this._logService.trace(`Tool ${toolCall.name} invocation result: ${JSON.stringify(result)}`);
@@ -32,7 +32,7 @@ import { ICommandService } from '../../commands/node/commandService';
import { getAgentForIntent, Intent } from '../../common/constants';
import { IConversationStore } from '../../conversationStore/node/conversationStore';
import { IIntentService } from '../../intents/node/intentService';
import { isAutoModel } from '../../../platform/endpoint/node/autoChatEndpoint';
import { isAutoExplainabilityHidden, isAutoModel } from '../../../platform/endpoint/node/autoChatEndpoint';
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
import { UnknownIntent } from '../../intents/node/unknownIntent';
import { formatAutoModeDetails, formatModelDetails } from '../../../platform/chat/common/chatModelDetails';
@@ -262,8 +262,8 @@ export class ChatParticipantRequestHandler {
result = await chatResult;
const endpoint = await this._endpointProvider.getChatEndpoint(this.request);
const creditsUsed = this._chatQuotaService.getCreditsForTurn(this.turn.id);
const hideAutoModelName = isAutoModel(endpoint) === 1
&& this._experimentationService.getTreatmentVariable<boolean>('copilotchat.hideAutoModelName') === true;
// Hiding explainability bills the turn to Auto rather than the model it picked.
const hideAutoModelName = isAutoModel(endpoint) === 1 && isAutoExplainabilityHidden(this._experimentationService);
if (hideAutoModelName) {
result.details = formatAutoModeDetails(creditsUsed, endpoint.multiplier);
} else if (this._authService.copilotToken?.isNoAuthUser) {
@@ -194,5 +194,7 @@ class NullAutomodeService implements IAutomodeService {
readonly onDidChangeAutoModeTierSupport = Event.None;
readonly onDidRoute = Event.None;
invalidateRouterCache(): void { }
}
@@ -116,16 +116,14 @@ export class ApplyPatchTool implements ICopilotTool<IApplyPatchToolParams> {
return trailingEmptyLines;
}
private async generateUpdateTextDocumentEdit(textDocument: TextDocumentSnapshot, file: string, change: FileChange, workspaceEdit: WorkspaceEdit) {
const uri = resolveToolInputPath(file, this.promptPathRepresentationService);
private async generateUpdateTextDocumentEdit(textDocument: TextDocumentSnapshot, uri: URI, movePath: URI | undefined, file: string, change: FileChange, workspaceEdit: WorkspaceEdit) {
const newContent = removeLeadingFilepathComment(change.newContent ?? '', textDocument.languageId, file);
const lines = newContent?.split('\n') ?? [];
let path = uri;
if (change.movePath) {
const newPath = resolveToolInputPath(change.movePath, this.promptPathRepresentationService);
workspaceEdit.renameFile(path, newPath, { overwrite: true });
path = newPath;
if (movePath) {
workspaceEdit.renameFile(path, movePath, { overwrite: true });
path = movePath;
}
workspaceEdit.replace(path, new Range(
new Position(0, 0),
@@ -150,7 +148,7 @@ export class ApplyPatchTool implements ICopilotTool<IApplyPatchToolParams> {
return path;
}
private async generateUpdateNotebookDocumentEdit(altDoc: NotebookDocumentSnapshot, uri: URI, file: string, change: FileChange) {
private async generateUpdateNotebookDocumentEdit(altDoc: NotebookDocumentSnapshot, uri: URI, movePath: URI | undefined, file: string, change: FileChange) {
// Notebooks can have various formats, it could be JSON, XML, Jupytext (which is a format that depends on the code cell language).
// Lets generate new content based on multiple formats.
const cellLanguage = getDefaultLanguage(altDoc.document) || 'python';
@@ -164,11 +162,10 @@ export class ApplyPatchTool implements ICopilotTool<IApplyPatchToolParams> {
].reduce((a, b) => a.length < b.length ? a : b);
const edits: (vscode.NotebookEdit | [vscode.Uri, vscode.TextEdit[]])[] = [];
if (change.movePath) {
const newPath = resolveToolInputPath(change.movePath, this.promptPathRepresentationService);
// workspaceEdit.renameFile(path, newPath, { overwrite: true });
if (movePath) {
// workspaceEdit.renameFile(path, movePath, { overwrite: true });
// TODO@joyceerhl: this is a hack, it doesnt't work for regular text files either.
uri = newPath;
uri = movePath;
}
const telemetryOptions: NotebookEditGenerationTelemtryOptions = {
@@ -271,6 +268,29 @@ export class ApplyPatchTool implements ICopilotTool<IApplyPatchToolParams> {
}
try {
const fileChanges = Object.entries(commit.changes).map(([file, changes]) => ({
file,
changes,
path: resolveToolInputPath(file, this.promptPathRepresentationService),
movePath: changes.movePath ? resolveToolInputPath(changes.movePath, this.promptPathRepresentationService) : undefined,
}));
for (const { changes, path, movePath } of fileChanges) {
const affectedUris = movePath
? [{ uri: path, contents: undefined }, { uri: movePath, contents: changes.newContent ?? '' }]
: [{ uri: path, contents: undefined }];
for (const { uri, contents } of affectedUris) {
const disallowedUriError = getDisallowedEditUriError(uri, this._promptContext?.allowedEditUris, this.promptPathRepresentationService);
if (disallowedUriError) {
const result = new ExtendedLanguageModelToolResult([
new LanguageModelTextPart(disallowedUriError),
]);
result.hasError = true;
return result;
}
await this.instantiationService.invokeFunction(accessor => assertFileNotContentExcluded(accessor, uri, undefined, contents));
}
}
// Map to track edit survival sessions by document URI
const editSurvivalTrackers = new ResourceMap<IEditSurvivalTrackingSession>();
@@ -291,18 +311,8 @@ export class ApplyPatchTool implements ICopilotTool<IApplyPatchToolParams> {
const workspaceEdit = new WorkspaceEdit();
const notebookEdits = new ResourceMap<(vscode.NotebookEdit | [vscode.Uri, vscode.TextEdit[]])[]>();
const deletedFiles = new ResourceSet();
for (const [file, changes] of Object.entries(commit.changes)) {
let path = resolveToolInputPath(file, this.promptPathRepresentationService);
const disallowedUriError = getDisallowedEditUriError(path, this._promptContext?.allowedEditUris, this.promptPathRepresentationService);
if (disallowedUriError) {
const result = new ExtendedLanguageModelToolResult([
new LanguageModelTextPart(disallowedUriError),
]);
result.hasError = true;
return result;
}
await this.instantiationService.invokeFunction(accessor => assertFileNotContentExcluded(accessor, path));
for (const { file, changes, path: sourcePath, movePath } of fileChanges) {
let path = sourcePath;
switch (changes.type) {
case ActionType.ADD: {
if (changes.newContent) {
@@ -325,7 +335,7 @@ export class ApplyPatchTool implements ICopilotTool<IApplyPatchToolParams> {
// We have found issues with the patches generated by Model for XML, Jupytext
// Possible there are other issues with other formats as well.
try {
const result = await this.generateUpdateNotebookDocumentEdit(document, path, file, changes);
const result = await this.generateUpdateNotebookDocumentEdit(document, path, movePath, file, changes);
notebookEdits.set(result.path, result.edits);
path = result.path;
if (changes.newContent) {
@@ -340,7 +350,7 @@ export class ApplyPatchTool implements ICopilotTool<IApplyPatchToolParams> {
}
}
else {
path = await this.generateUpdateTextDocumentEdit(document, file, changes, workspaceEdit);
path = await this.generateUpdateTextDocumentEdit(document, path, movePath, file, changes, workspaceEdit);
if (changes.newContent) {
updated = TextDocumentSnapshot.fromNewText(changes.newContent, document);
}
@@ -709,6 +709,8 @@ export async function applyEdit(
}
const ALWAYS_CHECKED_EDIT_PATTERNS: Readonly<Record<string, boolean>> = {
'**/.mcp.json': false,
'**/.npmrc': false,
'**/.vscode/*.json': false,
// Markdown files in these folders are loaded as custom agents; their
// frontmatter can declare a `hooks:` block that runs shell commands during
@@ -748,14 +748,24 @@ describe('makeUriConfirmationChecker', async () => {
expect(result).toBe(ConfirmationCheckResult.Sensitive); // Sensitive
});
test('always checks .vscode/*.json files', async () => {
test('always checks sensitive configuration files', async () => {
const workspaceFolder = URI.file('/workspace');
workspaceService = new TestWorkspaceService([workspaceFolder], []);
await configService.setNonExtensionConfig('chat.tools.edits.autoApprove', {
'**/.mcp.json': true,
'**/.npmrc': true,
});
const checker = makeUriConfirmationChecker(configService, workspaceService.getWorkspaceFolder.bind(workspaceService), customInstructionsService);
const settingsFile = URI.file('/workspace/.vscode/settings.json');
const result = await checker(settingsFile);
expect(result).toBe(ConfirmationCheckResult.Sensitive); // Sensitive - always requires confirmation
const files = [
URI.file('/workspace/.mcp.json'),
URI.file('/workspace/.npmrc'),
URI.file('/workspace/packages/nested/.npmrc'),
URI.file('/workspace/.vscode/settings.json'),
];
const results = await Promise.all(files.map(file => checker(file)));
expect(results).toEqual(files.map(() => ConfirmationCheckResult.Sensitive));
});
test('pattern precedence - later patterns override earlier ones', async () => {
@@ -205,13 +205,13 @@ function getInstructionsIndexFile(buildPromptContext: IBuildPromptContext, custo
}
export async function assertFileNotContentExcluded(accessor: ServicesAccessor, uri: URI, realPath?: URI): Promise<void> {
export async function assertFileNotContentExcluded(accessor: ServicesAccessor, uri: URI, realPath?: URI, contents?: string): Promise<void> {
const ignoreService = accessor.get(IIgnoreService);
const promptPathRepresentationService = accessor.get(IPromptPathRepresentationService);
if (await ignoreService.isCopilotIgnored(uri)) {
if (await ignoreService.isCopilotIgnored(uri, undefined, contents)) {
throw new Error(`File ${promptPathRepresentationService.getFilePath(uri)} is configured to be ignored by Copilot`);
}
if (realPath && !extUriBiasedIgnorePathCase.isEqual(realPath, uri) && await ignoreService.isCopilotIgnored(realPath)) {
if (realPath && !extUriBiasedIgnorePathCase.isEqual(realPath, uri) && await ignoreService.isCopilotIgnored(realPath, undefined, contents)) {
throw new Error(`File ${promptPathRepresentationService.getFilePath(realPath)} is configured to be ignored by Copilot`);
}
}
@@ -6,23 +6,38 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import { beforeEach, expect, it, suite } from 'vitest';
import { IIgnoreService, NullIgnoreService } from '../../../../../platform/ignore/common/ignoreService';
import { ITestingServicesAccessor } from '../../../../../platform/test/node/services';
import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';
import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';
import { ChatResponseStreamImpl } from '../../../../../util/common/chatResponseStreamImpl';
import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument';
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
import { ResourceSet } from '../../../../../util/vs/base/common/map';
import { assertType } from '../../../../../util/vs/base/common/types';
import { URI } from '../../../../../util/vs/base/common/uri';
import { SyncDescriptor } from '../../../../../util/vs/platform/instantiation/common/descriptors';
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
import { ChatResponseTextEditPart } from '../../../../../vscodeTypes';
import { ChatResponseTextEditPart, ExtendedLanguageModelToolResult } from '../../../../../vscodeTypes';
import { ChatVariablesCollection } from '../../../../prompt/common/chatVariablesCollection';
import { WorkingCopyOriginalDocument } from '../../../../prompts/node/inline/workingCopies';
import { createExtensionUnitTestingServices } from '../../../../test/node/services';
import { ApplyPatchTool, healedPatchAffectsSameFiles, IApplyPatchToolParams } from '../../../node/applyPatchTool';
class TestIgnoreService extends NullIgnoreService {
readonly checkedUris: string[] = [];
constructor(private readonly ignoredUris: ResourceSet) {
super();
}
override async isCopilotIgnored(file: URI): Promise<boolean> {
this.checkedUris.push(file.toString());
return this.ignoredUris.has(file);
}
}
suite('ApplyPatch Tool', () => {
let accessor: ITestingServicesAccessor;
@@ -43,6 +58,39 @@ suite('ApplyPatch Tool', () => {
accessor = services.createTestingAccessor();
});
function createMovePatch(destination: URI): IApplyPatchToolParams {
return {
explanation: 'Condense the offSide language array and move the file.',
input: [
'*** Begin Patch',
`*** Update File: ${path}`,
`*** Move to: ${destination.fsPath}`,
'@@',
'-\tconst offSide = [',
'-\t\t\'clojure\',',
'-\t\t\'coffeescript\',',
'-\t\t\'fsharp\',',
'-\t\t\'latex\',',
'-\t\t\'markdown\',',
'-\t\t\'pug\',',
'-\t\t\'python\',',
'-\t\t\'sql\',',
'-\t\t\'yaml\',',
'-\t].includes(languageId.toLowerCase());',
'+\tconst offSide = [\'clojure\',\'coffeescript\',\'fsharp\',\'latex\',\'markdown\',\'pug\',\'python\',\'sql\',\'yaml\'].includes(languageId.toLowerCase());',
'*** End Patch',
].join('\n'),
};
}
function createRecordingStream(editedUris: string[]): ChatResponseStreamImpl {
return new ChatResponseStreamImpl(part => {
if (part instanceof ChatResponseTextEditPart && part.edits.length > 0) {
editedUris.push(part.uri.toString());
}
}, () => { }, () => { }, undefined, undefined, () => Promise.resolve(undefined));
}
it('makes changes atomically', async () => {
const input: IApplyPatchToolParams = JSON.parse(`{
@@ -88,6 +136,86 @@ suite('ApplyPatch Tool', () => {
});
it('rejects a content-excluded move destination before emitting edits', async () => {
const services = createExtensionUnitTestingServices();
const destination = URI.file(join(__dirname, 'fixtures/ignored.ts'));
const ignoreService = new TestIgnoreService(new ResourceSet([destination]));
services.define(IIgnoreService, ignoreService);
const content = String(readFileSync(path));
const testDoc = createTextDocumentData(fileTsUri, content, 'ts').document;
services.define(IWorkspaceService, new SyncDescriptor(
TestWorkspaceService, [[fileTsUri], [testDoc]]
));
const localAccessor = services.createTestingAccessor();
const tool = localAccessor.get(IInstantiationService).createInstance(ApplyPatchTool);
const editedUris: string[] = [];
const input = await tool.resolveInput(createMovePatch(destination), {
history: [],
stream: createRecordingStream(editedUris),
query: 'change and move the file',
chatVariables: new ChatVariablesCollection([]),
});
const result = await tool.invoke({ input, toolInvocationToken: undefined }, CancellationToken.None);
expect({
hasError: result instanceof ExtendedLanguageModelToolResult ? result.hasError : undefined,
editedUris,
checkedUris: ignoreService.checkedUris,
}).toEqual({
hasError: true,
editedUris: [],
checkedUris: [fileTsUri.toString(), destination.toString()],
});
});
it('rejects a move destination outside allowedEditUris before emitting edits', async () => {
const destination = URI.file(join(__dirname, 'fixtures/disallowed.ts'));
const tool = accessor.get(IInstantiationService).createInstance(ApplyPatchTool);
const editedUris: string[] = [];
const input = await tool.resolveInput(createMovePatch(destination), {
history: [],
stream: createRecordingStream(editedUris),
query: 'change and move the file',
chatVariables: new ChatVariablesCollection([]),
allowedEditUris: new ResourceSet([fileTsUri]),
});
const result = await tool.invoke({ input, toolInvocationToken: undefined }, CancellationToken.None);
expect({
hasError: result instanceof ExtendedLanguageModelToolResult ? result.hasError : undefined,
editedUris,
}).toEqual({
hasError: true,
editedUris: [],
});
});
it('applies a move when the source and destination are allowed', async () => {
const destination = URI.file(join(__dirname, 'fixtures/allowed.ts'));
const tool = accessor.get(IInstantiationService).createInstance(ApplyPatchTool);
const editedUris: string[] = [];
const input = await tool.resolveInput(createMovePatch(destination), {
history: [],
stream: createRecordingStream(editedUris),
query: 'change and move the file',
chatVariables: new ChatVariablesCollection([]),
allowedEditUris: new ResourceSet([fileTsUri, destination]),
});
const result = await tool.invoke({ input, toolInvocationToken: undefined }, CancellationToken.None);
expect({
hasError: result instanceof ExtendedLanguageModelToolResult ? result.hasError : undefined,
editedUris,
}).toEqual({
hasError: false,
editedUris: [destination.toString()],
});
});
suite('healedPatchAffectsSameFiles', () => {
const makePatch = (lines: string[]) => ['*** Begin Patch', ...lines, '*** End Patch'].join('\n');
@@ -132,3 +132,10 @@ export function isAutoModel(endpoint: IChatEndpoint | undefined): number {
}
return (endpoint.model === AutoChatEndpoint.pseudoModelId || endpoint instanceof AutoChatEndpoint) ? 1 : -1;
}
/** Kept in sync with the workbench copy in `chatAutoModeExplainability.ts`. */
const HIDE_AUTO_EXPLAINABILITY_TREATMENT = 'copilotchat.hideAutoExplainability';
export function isAutoExplainabilityHidden(expService: IExperimentationService): boolean {
return expService.getTreatmentVariable<boolean>(HIDE_AUTO_EXPLAINABILITY_TREATMENT) === true;
}
@@ -3,13 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { ChatRequest } from 'vscode';
import type { ChatRequest, ChatResponseStream } from 'vscode';
import { createServiceIdentifier } from '../../../util/common/services';
import { TaskSingler } from '../../../util/common/taskSingler';
import { Emitter, type Event } from '../../../util/vs/base/common/event';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { Disposable, type IDisposable } from '../../../util/vs/base/common/lifecycle';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { ChatLocation } from '../../../vscodeTypes';
import { ChatLocation, ChatResponseAutoModeResolutionPart } from '../../../vscodeTypes';
import { IAuthenticationService } from '../../authentication/common/authentication';
import { ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';
import { ILogService } from '../../log/common/logService';
@@ -67,6 +67,30 @@ export interface AutoModePickerMetadata {
discountRange: { low: number; high: number };
}
/** A routing state change for one request: `endpoint` is unset while the router is still deciding. */
export interface IAutoModeRoutingState {
readonly requestId: string | undefined;
readonly endpoint: IChatEndpoint | undefined;
}
/**
* Reports Auto's routing rounds into a turn's response stream. Install this
* before the turn resolves any endpoint the first route happens during
* endpoint resolution, and a route that already finished cannot be replayed.
*/
export function reportAutoModeRouting(
request: ChatRequest,
stream: ChatResponseStream,
automodeService: IAutomodeService,
): IDisposable {
return automodeService.onDidRoute(e => {
if (e.requestId !== request.id) {
return;
}
stream.push(new ChatResponseAutoModeResolutionPart(e.endpoint && { id: e.endpoint.model, name: e.endpoint.name }));
});
}
export interface IAutomodeService {
readonly _serviceBrand: undefined;
@@ -101,6 +125,13 @@ export interface IAutomodeService {
*/
readonly onDidChangeAutoModeTierSupport: Event<void>;
/**
* Fires when a request starts routing and again once it resolves. Only real
* routing rounds fire a cached endpoint is silent and Auto can route
* several times in a turn, e.g. after compaction.
*/
readonly onDidRoute: Event<IAutoModeRoutingState>;
/**
* Marks the router cache for this conversation as needing re-evaluation.
* The next call to {@link resolveAutoModeEndpoint} will re-run the router
@@ -121,6 +152,8 @@ export class AutomodeService extends Disposable implements IAutomodeService {
private static readonly CACHE_MAX_ENTRIES = 50;
private readonly _onDidChangeAutoModeTierSupport = this._register(new Emitter<void>());
readonly onDidChangeAutoModeTierSupport = this._onDidChangeAutoModeTierSupport.event;
private readonly _onDidRoute = this._register(new Emitter<IAutoModeRoutingState>());
readonly onDidRoute = this._onDidRoute.event;
/** Last announced {@link areAutoModeTiersSupported}. See {@link _updateAutoModeTierSupport}. */
private _tierSupportAnnounced = false;
@@ -236,6 +269,23 @@ export class AutomodeService extends Disposable implements IAutomodeService {
knownEndpoints: IChatEndpoint[],
conversationId: string,
entry: AutoModeCacheEntry | undefined,
): Promise<IChatEndpoint> {
// Brackets the round so every way of settling it — including the cached
// fallback below — reports the endpoint it settled on. A throw reports
// nothing, leaving the turn's row unresolved for the UI to drop.
this._onDidRoute.fire({ requestId: chatRequest?.id, endpoint: undefined });
const endpoint = await this._route(prompt, tier, chatRequest, knownEndpoints, conversationId, entry);
this._onDidRoute.fire({ requestId: chatRequest?.id, endpoint });
return endpoint;
}
private async _route(
prompt: string,
tier: AutoModeTier | undefined,
chatRequest: IAutoModeRoutingRequest | undefined,
knownEndpoints: IChatEndpoint[],
conversationId: string,
entry: AutoModeCacheEntry | undefined,
): Promise<IChatEndpoint> {
// The session this mints belongs to the account signed in right now, so
// anything resolved here is void if that account changes mid-flight.
@@ -33,7 +33,7 @@ export interface IIgnoreService {
init(): Promise<void>;
isCopilotIgnored(file: URI, token?: CancellationToken): Promise<boolean>;
isCopilotIgnored(file: URI, token?: CancellationToken, contents?: string): Promise<boolean>;
asMinimatchPattern(): Promise<string | undefined>;
}
@@ -107,7 +107,7 @@ export class BaseIgnoreService implements IIgnoreService {
return this._remoteContentExclusions?.isRegexContextExclusionsEnabled ?? false;
}
public async isCopilotIgnored(file: URI, token?: CancellationToken): Promise<boolean> {
public async isCopilotIgnored(file: URI, token?: CancellationToken, contents?: string): Promise<boolean> {
this.syncEnablement();
if (!this._copilotIgnoreEnabled) {
return false;
@@ -116,7 +116,7 @@ export class BaseIgnoreService implements IIgnoreService {
// report every file as allowed for the whole of extension startup.
await this.init();
const localCopilotIgnored = this._copilotIgnoreFiles.isIgnored(file);
return localCopilotIgnored || await (this._remoteContentExclusions?.isIgnored(file, token) ?? false);
return localCopilotIgnored || await (this._remoteContentExclusions?.isIgnored(file, token, contents) ?? false);
}
@@ -173,10 +173,13 @@ export class RemoteContentExclusion implements IDisposable {
));
}
public async isIgnored(file: URI, token: CancellationToken = CancellationToken.None): Promise<boolean> {
const memoised = this.memoisedVerdict(file);
if (memoised !== undefined) {
return memoised;
public async isIgnored(file: URI, token: CancellationToken = CancellationToken.None, contents?: string): Promise<boolean> {
const hasProvidedContents = contents !== undefined;
if (!hasProvidedContents) {
const memoised = this.memoisedVerdict(file);
if (memoised !== undefined) {
return memoised;
}
}
// Try to find the repository from the cache first to avoid expensive git extension calls
@@ -217,8 +220,7 @@ export class RemoteContentExclusion implements IDisposable {
return true;
}
}
let fileContents: string = '';
let fileContentHash: string = '';
let fileContents = contents?.slice(0, 1024);
// Unscoped organization rules are keyed under the non-git pseudo repo and apply to any file.
// Their globs already reach every file, so content rules must be evaluated against them too.
const regexRuleSources = repoMetadata.fetchUrls.includes(NON_GIT_FILE_KEY)
@@ -232,31 +234,34 @@ export class RemoteContentExclusion implements IDisposable {
const { ifAnyMatch, ifNoneMatch } = this._contentExclusionCache.get(fetchUrl) ?? { ifAnyMatch: [], ifNoneMatch: [] };
// We only want to read the file if we absolutely must as it can be expensive
if (ifAnyMatch.length > 0 || ifNoneMatch.length > 0) {
if (!fileContents) {
if (fileContents === undefined) {
try {
// Read the file contents and hash it so we can cache the result - Only reads up to 1KB of the file, as reading too much can be expensive and regex exclusions are normally header based
// Note: This feature is internal only so we can adapt the implementation as needed without breaking clients.
const fileContentOrBuffer = await this._fileReadLimiter.queue(() => readFileFromTextBufferOrFS(this._fileSystemService, this._workspaceService, file, 1024));
fileContents = typeof fileContentOrBuffer === 'string' ? fileContentOrBuffer : new TextDecoder().decode(fileContentOrBuffer);
fileContentHash = await createSha256Hash(fileContents);
regexCacheKey = `${regexScope}\n${fileContentHash}`;
// Cache hit for these file contents, no need to run the regex patterns
const cachedRegexVerdict = this._ignoreRegexResultCache.get(regexCacheKey);
if (cachedRegexVerdict && cachedRegexVerdict.generation === generation) {
return cachedRegexVerdict.verdict;
}
} catch {
// We failed to read the file, so it should just be ignored as we have no idea what the contents are or if it exists
return true;
}
}
if (!regexCacheKey) {
const fileContentHash = await createSha256Hash(fileContents);
regexCacheKey = `${regexScope}\n${fileContentHash}`;
// Cache hit for these file contents, no need to run the regex patterns
const cachedRegexVerdict = this._ignoreRegexResultCache.get(regexCacheKey);
if (cachedRegexVerdict && cachedRegexVerdict.generation === generation) {
return cachedRegexVerdict.verdict;
}
}
}
if (ifAnyMatch.length > 0 && fileContents && ifAnyMatch.some(pattern => pattern.test(fileContents))) {
const contentsToCheck = fileContents;
if (ifAnyMatch.length > 0 && contentsToCheck !== undefined && ifAnyMatch.some(pattern => pattern.test(contentsToCheck))) {
this._logService.debug(`File ${file.path} is ignored by content exclusion rule ifAnyMatch`);
this._ignoreRegexResultCache.set(regexCacheKey, { verdict: true, generation });
return true;
}
if (ifNoneMatch.length > 0 && fileContents && !ifNoneMatch.some(pattern => pattern.test(fileContents))) {
if (ifNoneMatch.length > 0 && contentsToCheck !== undefined && !ifNoneMatch.some(pattern => pattern.test(contentsToCheck))) {
this._logService.debug(`File ${file.path} is ignored by content exclusion rule ifNoneMatch`);
this._ignoreRegexResultCache.set(regexCacheKey, { verdict: true, generation });
return true;
@@ -265,7 +270,7 @@ export class RemoteContentExclusion implements IDisposable {
// Memoise a negative verdict only once the rules have loaded and the repository is settled.
// Doing it earlier would keep the file allowed long after its real rules become known.
if (rulesLoaded && repoSettled) {
if (rulesLoaded && repoSettled && !hasProvidedContents) {
this._ignoreGlobResultCache.set(file, { verdict: false, generation });
// Only meaningful when regex rules forced us to read (and hash) the file.
if (regexCacheKey) {
@@ -683,6 +683,21 @@ suite('RemoteContentExclusion', () => {
expect(await remoteContentExclusion.isIgnored(file, CancellationToken.None)).toBe(true);
});
test('evaluates provided contents for a file that does not exist', async () => {
routeToRepos(['/workspace/repo-a']);
respondWithRules({ '/workspace/repo-a': { ifAnyMatch: ['CONFIDENTIAL'], ifNoneMatch: ['PUBLIC'] } });
expect({
confidential: await remoteContentExclusion.isIgnored(file, CancellationToken.None, '// CONFIDENTIAL'),
unmarked: await remoteContentExclusion.isIgnored(file, CancellationToken.None, 'export const a = 1;'),
public: await remoteContentExclusion.isIgnored(file, CancellationToken.None, '// PUBLIC'),
}).toEqual({
confidential: true,
unmarked: true,
public: false,
});
});
test('reports regex exclusions only once a regex rule has been fetched', async () => {
routeToRepos(['/workspace/repo-a']);
respondWithRules({ '/workspace/repo-a': { ifAnyMatch: ['CONFIDENTIAL'] } });
@@ -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.
*--------------------------------------------------------------------------------------------*/
/**
* Matches a single escape sequence in serialized JSON, capturing the part after the backslash.
*
* Matching *every* escape rather than only the surrogate ones is what keeps this both correct and
* linear. An escaped backslash is consumed whole, so text that merely looks like an escape (a
* literal `\ud83d`, which `JSON.stringify` writes as `\\ud83d`) can never be mistaken for one, and
* no position is ever rescanned. Matching `\\+u...` instead would be quadratic in the length of a
* run of backslashes, which is reachable from tool output.
*/
const JSON_ESCAPE = /\\(u[dD][89a-fA-F][0-9a-fA-F]{2}|[\s\S])/g;
/** The escaped form of the Unicode replacement character, `\uFFFD`. */
const UNICODE_REPLACEMENT_ESCAPE = '\\ufffd';
/**
* Serializes a request body to JSON that can be decoded as UTF-8 by the receiving service.
*
* `JSON.stringify` turns an unpaired surrogate into a `\uXXXX` escape rather than failing. That
* escape has no UTF-8 encoding, so strict server-side JSON parsers reject the whole body with a
* `400`. Because a rejected body is usually replayed conversation history, a single bad code unit
* keeps failing every later request in that session until the conversation is abandoned.
*
* The repair happens on the serialized JSON rather than on the input because a `JSON.stringify`
* replacer cannot rewrite property names, only values.
*
* @throws if `value` has no JSON representation at all, such as `undefined` or a function.
*/
export function stringifyJsonBody(value: unknown): string {
const serialized = JSON.stringify(value);
if (typeof serialized !== 'string') {
throw new Error(`Illegal arguments! A value of type '${typeof value}' has no JSON representation!`);
}
// `JSON.stringify` escapes a surrogate code unit only when it is unpaired, and always writes the
// escape in lowercase; well-formed pairs and every other non-ASCII character are written as
// literal characters. Bodies are usually replayed conversation history and almost never contain
// such an escape, so skip the scan entirely rather than walking megabytes for nothing. Text that
// itself contains a `\ud` sequence merely costs a redundant scan, so this check is allowed to be
// over-eager but never under-eager.
if (!serialized.includes('\\ud')) {
return serialized;
}
return serialized.replace(JSON_ESCAPE, (match, escape: string) =>
escape.length === 1 ? match : UNICODE_REPLACEMENT_ESCAPE);
}
@@ -8,6 +8,7 @@ import { generateUuid } from '../../../util/vs/base/common/uuid';
import { IEnvService } from '../../env/common/envService';
import { collectSingleLineErrorMessage } from '../../log/common/logService';
import { CacheStatus, FetcherId, FetchOptions, IAbortController, isAbortError, PaginationOptions, ReportFetchEvent, Response, safeGetHostname } from '../common/fetcherService';
import { stringifyJsonBody } from '../common/jsonBody';
import { IFetcher, userAgentLibraryHeader } from '../common/networking';
import { VSCODE_CACHE_STATUS_HEADER } from './taggedCacheInterceptor';
@@ -43,7 +44,7 @@ export abstract class BaseFetchFetcher implements IFetcher {
throw new Error(`Illegal arguments! Cannot pass in both 'body' and 'json'!`);
}
headers['Content-Type'] = 'application/json';
body = JSON.stringify(options.json);
body = stringifyJsonBody(options.json);
}
const method = options.method || 'GET';
@@ -16,6 +16,7 @@ import { ICAPIClientService } from '../../endpoint/common/capiClient';
import { ILogService, collectSingleLineErrorMessage } from '../../log/common/logService';
import { ITelemetryService } from '../../telemetry/common/telemetry';
import { HeadersImpl, IHeaders, WebSocketConnection } from '../common/fetcherService';
import { stringifyJsonBody } from '../common/jsonBody';
import { IEndpointBody } from '../common/networking';
import { getResponsesApiCompactionThresholdFromBody } from '../../endpoint/node/responsesApi';
import { ChatWebSocketRequestOutcome, ChatWebSocketTelemetrySender } from './chatWebSocketTelemetry';
@@ -680,7 +681,7 @@ class ChatWebSocketConnection extends Disposable implements IChatWebSocketConnec
...rest,
initiator: options.userInitiated ? 'user' : 'agent',
};
const serializedMessage = JSON.stringify(message);
const serializedMessage = stringifyJsonBody(message);
const sentMessageCharacters = serializedMessage.length;
this._totalSentMessageCount += 1;
this._totalSentCharacters += sentMessageCharacters;
@@ -10,6 +10,7 @@ import { generateUuid } from '../../../util/vs/base/common/uuid';
import { IEnvService } from '../../env/common/envService';
import { collectSingleLineErrorMessage } from '../../log/common/logService';
import { FetchOptions, HeadersImpl, IAbortController, IHeaders, PaginationOptions, ReportFetchEvent, Response, safeGetHostname } from '../common/fetcherService';
import { stringifyJsonBody } from '../common/jsonBody';
import { IFetcher, userAgentLibraryHeader } from '../common/networking';
export class NodeFetcher implements IFetcher {
@@ -40,7 +41,7 @@ export class NodeFetcher implements IFetcher {
throw new Error(`Illegal arguments! Cannot pass in both 'body' and 'json'!`);
}
headers['Content-Type'] = 'application/json';
body = JSON.stringify(options.json);
body = stringifyJsonBody(options.json);
}
const method = options.method || 'GET';
@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* 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 { suite, test } from 'vitest';
import { stringifyJsonBody } from '../../common/jsonBody';
suite('stringifyJsonBody', () => {
test('emits a body that a strict UTF-8 parser accepts', () => {
const body = {
// A tool result truncated in the middle of an emoji, as a character-count limit would do.
truncated: `head${'🙂'.repeat(3).slice(0, 5)}tail`,
// A lone surrogate in a property name, which a `JSON.stringify` replacer cannot reach.
['key\uD83D']: 'value',
// A lone surrogate right after a literal backslash, so the escape it produces is preceded
// by an odd-length backslash run.
afterBackslash: '\\\uDE42',
};
const serialized = stringifyJsonBody(body);
assert.deepStrictEqual(
{
hasSurrogateEscape: /\\u[dD][89a-fA-F]/.test(serialized),
survivesUtf8: Buffer.from(serialized, 'utf8').toString('utf8') === serialized,
parsed: JSON.parse(serialized),
},
{
hasSurrogateEscape: false,
survivesUtf8: true,
parsed: {
truncated: 'head🙂🙂\uFFFDtail',
'key\uFFFD': 'value',
afterBackslash: '\\\uFFFD',
},
}
);
});
test('matches JSON.stringify when the payload is already well-formed', () => {
// `literalEscapeText` is text rather than an escape: `JSON.stringify` doubles its backslash,
// so the sanitizing pass must consume the pair and leave it byte-for-byte identical.
const body = { emoji: 'a 🙂 b', literalEscapeText: 'not an escape: \\ud83d', control: '\n\t"' };
assert.strictEqual(stringifyJsonBody(body), JSON.stringify(body));
});
test('scans a long run of backslashes in linear time', () => {
// Guards against reintroducing a pattern like `(\\+)u...`, whose backtracking is quadratic in
// the length of a backslash run and turns this reachable tool output into a denial of service.
// A linear scan finishes in single-digit milliseconds; the quadratic one needs over a minute,
// so the suite timeout is what fails here rather than a flaky duration assertion.
const body = { content: '\\'.repeat(200_000) + 'ud8' };
assert.strictEqual(stringifyJsonBody(body), JSON.stringify(body));
});
test('rejects a value that has no JSON representation', () => {
const attempt = (value: unknown) => {
try {
stringifyJsonBody(value);
return 'serialized';
} catch (error) {
return (error as Error).message;
}
};
assert.deepStrictEqual(
{ undefined: attempt(undefined), function: attempt(() => { }), object: attempt({}) },
{
undefined: `Illegal arguments! A value of type 'undefined' has no JSON representation!`,
function: `Illegal arguments! A value of type 'function' has no JSON representation!`,
object: 'serialized',
}
);
});
});
@@ -124,6 +124,26 @@ suite('AgentInstructionsLocator', () => {
expect(paths).toContain(`${parentFolder}/AGENTS.md`);
});
test('should collect parent instructions through a submodule .git file', async () => {
await mockFiles(fileSystem, [
{ path: `${parentFolder}/.git/HEAD`, contents: ['ref: refs/heads/main'] },
{ path: `${rootFolder}/.git`, contents: ['gitdir: ../.git/modules/submodule'] },
{ path: `${parentFolder}/AGENTS.md`, contents: ['Parent agent guidelines'] },
{ path: `${parentFolder}/.github/copilot-instructions.md`, contents: ['Parent copilot instructions'] },
{ path: `${rootFolder}/src/file.ts`, contents: ['console.log("test");'] },
]);
workspaceService.setTrusted(parentFolderUri, true);
await configService.setNonExtensionConfig(PromptConfig.USE_CUSTOMIZATIONS_IN_PARENT_REPOS, true);
await configService.setConfig(ConfigKey.UseInstructionFiles, true);
const result = await locator.listAgentInstructions(CancellationToken.None);
expect(result.map(file => file.uri.path)).toEqual([
`${parentFolder}/.github/copilot-instructions.md`,
`${parentFolder}/AGENTS.md`,
]);
});
test('copilot-instructions and AGENTS.md', async () => {
// Files at the workspace root only — `useCustomizationsInParentRepositories`
// is left at its default (off) so the locator only inspects `rootFolder`.
@@ -189,7 +189,8 @@ export class AgentInstructionsLocator extends Disposable {
while (true) {
try {
const gitFolder = joinPath(current, '.git');
const isRepoRoot = await this.fileSystemService.stat(gitFolder).then(() => true, () => false);
const gitStat = await this.fileSystemService.stat(gitFolder).then(stat => stat, () => undefined);
const isRepoRoot = gitStat !== undefined && (gitStat.type & FileType.Directory) !== 0;
if (isRepoRoot) {
// Only include the repo root (and any intermediate parents) if the user has explicitly trusted it.
const trusted = await this.workspaceService.isResourceTrusted(current);
@@ -194,15 +194,9 @@ export class ChatResponsePullRequestPart {
export class ChatResponseAutoModeResolutionPart {
resolvedModel: string;
resolvedModelName: string;
predictedLabel: string;
confidence: number;
constructor(resolvedModel: string, resolvedModelName: string, predictedLabel: string, confidence: number) {
resolvedModel: { id: string; name: string } | undefined;
constructor(resolvedModel?: { id: string; name: string }) {
this.resolvedModel = resolvedModel;
this.resolvedModelName = resolvedModelName;
this.predictedLabel = predictedLabel;
this.confidence = confidence;
}
}
@@ -44,7 +44,7 @@ interface InitialState {
readonly documentVersion: number;
readonly readonly: boolean;
readonly richLinksEnabled: boolean;
readonly linkPresentationRules: readonly { id: string; source: string; flags: string; initialKind: LinkPresentationKind }[];
readonly linkPresentationRules: readonly { id: string; source: string; flags: string; kind: LinkPresentationKind }[];
}
class Editor extends Disposable {
@@ -21,19 +21,19 @@ type WebviewLinkPresentation = LinkPresentation & { readonly isLoading?: boolean
export class WebviewLinkPresentationProvider extends Disposable implements ILinkPresentationProvider {
readonly #entries = new Map<string, LinkPresentationEntry>();
readonly #rules: readonly { id: string; uriPattern: RegExp; initialKind: LinkPresentationKind }[];
readonly #rules: readonly { id: string; uriPattern: RegExp; kind: LinkPresentationKind }[];
readonly #postMessage: (message: unknown) => void;
#syncScheduled = false;
constructor(
rules: readonly { id: string; source: string; flags: string; initialKind: LinkPresentationKind }[],
rules: readonly { id: string; source: string; flags: string; kind: LinkPresentationKind }[],
postMessage: (message: unknown) => void,
) {
super();
this.#rules = rules.map(rule => ({
id: rule.id,
uriPattern: new RegExp(rule.source, rule.flags),
initialKind: rule.initialKind,
kind: rule.kind,
}));
this.#postMessage = postMessage;
}
@@ -48,7 +48,7 @@ export class WebviewLinkPresentationProvider extends Disposable implements ILink
if (!entry) {
entry = {
presentation: observableValue(`linkPresentation:${url}`, {
kind: rule.initialKind,
kind: rule.kind,
isLoading: true,
}),
references: 0,
@@ -47,9 +47,14 @@
"contributes": {
"linkPresentationProviders": [
{
"id": "markdown.linkPresentations",
"initialKind": "resource",
"uriPattern": "^(?:(?:file|vscode-remote|vscode-vfs):[^?#]*|commit:[^?#]+|https?://[^\\s?#]+/(?:commit|-/commit)/[^/?#]+|https://github\\.com/[^/?#]+/[^/?#]+(?:/(?:issues/[^/?#]+|pull/[^/?#]+|tree/[^?#]+|blob/[^?#]+))?|(?!(?:[a-z][a-z0-9+.-]*:|#))[^?#]+)(?:[?#].*)?$"
"id": "markdown.gitCommitLinkPresentations",
"kind": "commit",
"uriPattern": "^(?:commit:[^?#]+|https?://[^\\s?#]+/(?:commit|-/commit)/[^/?#]+)(?:[?#].*)?$"
},
{
"id": "markdown.workspaceFileLinkPresentations",
"kind": "file",
"uriPattern": "^(?:(?:file|vscode-remote|vscode-vfs):[^?#]*|(?!(?:[a-z][a-z0-9+.-]*:|#))[^?#]+)(?:[?#].*)?$"
}
],
"notebookRenderer": [
@@ -1271,7 +1276,7 @@
"properties": {
"markdown.experimental.richLinks.enabled": {
"type": "boolean",
"default": false,
"default": true,
"description": "%configuration.markdown.experimental.richLinks.enabled%",
"scope": "window",
"tags": [
@@ -47,7 +47,7 @@ export function activateShared(
context.subscriptions.push(registerMarkdownLanguageFeatures(client, commandManager, engine));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine));
const linkPresentationService = createSharedLinkPresentationService(context.globalState, logger);
const linkPresentationService = createSharedLinkPresentationService(logger);
context.subscriptions.push(
linkPresentationService,
registerLinkPresentationProvider(linkPresentationService),
@@ -24,6 +24,12 @@ export class VsCodeOutputLogger extends Disposable implements ILogger {
}
public trace(title: string, message: string, data?: unknown): void {
this.#outputChannel.trace(`${title}: ${message}`, ...(data ? [JSON.stringify(data, null, 4)] : []));
if (!this.#outputChannelValue && vscode.env.logLevel !== vscode.LogLevel.Trace) {
return;
}
const outputChannel = this.#outputChannel;
if (outputChannel.logLevel === vscode.LogLevel.Trace) {
outputChannel.trace(`${title}: ${message}`, ...(data ? [JSON.stringify(data, null, 4)] : []));
}
}
}
@@ -6,6 +6,7 @@
import type { LinkPresentation } from '@vscode/markdown-editor';
import type { IObservable } from '@vscode/observables';
import * as vscode from 'vscode';
import { buildGitCommitLookupFailurePresentation, buildGitCommitPresentation, buildLoadingLinkPresentation, type GitCommitPresentationData } from './linkPresentationBuilders';
import { createAsyncLinkPresentation, decodeUrlPathSegments, ImmutableLinkPresentationCache, type LinkPresentationResolver, type LinkPresentationResolverContext } from './linkPresentationResolver';
export class GitLinkPresentationResolver implements LinkPresentationResolver {
@@ -28,18 +29,13 @@ export class GitLinkPresentationResolver implements LinkPresentationResolver {
return createAsyncLinkPresentation(
href,
{
kind: 'commit',
status: { kind: 'pending', label: 'Loading' },
},
buildLoadingLinkPresentation('commit'),
context,
() => this.#cache.get(href, () => this.#resolve(target)),
error => ({
kind: 'commit',
status: { kind: 'error', label: 'Not available' },
tooltip: error instanceof Error ? error.message : 'The Git commit could not be resolved.',
ariaLabel: `Git commit ${target.sha.slice(0, 7)} could not be resolved`,
}),
error => buildGitCommitLookupFailurePresentation(
target.sha.slice(0, 7),
error instanceof Error ? error.message : 'The Git commit could not be resolved.',
),
[context.onDidRequestRefresh, this.#onDidChangeRepositories.event],
);
}
@@ -80,7 +76,7 @@ export class GitLinkPresentationResolver implements LinkPresentationResolver {
}
async #resolve(target: GitCommitTarget): Promise<LinkPresentation> {
return getGitCommitPresentation((await this.#findCommit(target)).commit);
return buildGitCommitPresentation((await this.#findCommit(target)).commit);
}
async #findCommit(target: GitCommitTarget): Promise<GitCommitResult> {
@@ -224,30 +220,11 @@ interface GitRemote {
readonly pushUrl?: string;
}
interface GitCommit {
readonly hash: string;
readonly message: string;
readonly shortStat?: {
readonly insertions: number;
readonly deletions: number;
};
}
interface GitCommit extends GitCommitPresentationData { }
interface GitCommitResult {
readonly repository: GitRepository;
readonly commit: GitCommit;
}
export function getGitCommitPresentation(commit: GitCommit): LinkPresentation {
const title = commit.message.split(/\r?\n/, 1)[0];
const insertions = commit.shortStat?.insertions ?? 0;
const deletions = commit.shortStat?.deletions ?? 0;
const shortHash = commit.hash.slice(0, 7);
return {
kind: 'commit',
detail: title,
// TODO: Include insertion and deletion counts once the Markdown editor package supports them.
tooltip: `${shortHash} · ${title} · ${insertions} insertions, ${deletions} deletions`,
ariaLabel: `Commit ${shortHash}, ${insertions} insertions and ${deletions} deletions: ${title}`,
};
}
export { buildGitCommitPresentation as getGitCommitPresentation } from './linkPresentationBuilders';
@@ -1,543 +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 { LinkPresentationStatus } from '@vscode/markdown-editor';
import type { IObservable } from '@vscode/observables';
import * as vscode from 'vscode';
import { createAsyncLinkPresentation, decodeUrlPathSegments, LinkPresentationCache, type LinkPresentation, type LinkPresentationResolver, type LinkPresentationResolverContext } from './linkPresentationResolver';
const githubRepositoryScope = 'repo';
export class GitHubLinkPresentationResolver implements LinkPresentationResolver {
readonly refreshOnInterval = true;
readonly #cache: LinkPresentationCache;
readonly #onDidChangeAuthentication = new vscode.EventEmitter<void>();
readonly #authenticationSubscription: vscode.Disposable;
#accessToken: Promise<string> | undefined;
constructor(cache: LinkPresentationCache) {
this.#cache = cache;
this.#authenticationSubscription = vscode.authentication.onDidChangeSessions(event => {
if (event.provider.id === 'github') {
this.#accessToken = undefined;
this.#cache.clear();
this.#onDidChangeAuthentication.fire();
}
});
}
resolve(href: string, context: LinkPresentationResolverContext): IObservable<LinkPresentation> | undefined {
const target = parseGitHubTarget(href);
if (!target) {
return undefined;
}
const persisted = this.#cache.getPersisted(href);
const loadingPresentation: LinkPresentation = {
kind: target.kind === 'tree' ? 'resource' : target.kind,
status: { kind: 'pending', label: 'Loading' },
};
return createAsyncLinkPresentation(
href,
persisted ?? loadingPresentation,
context,
() => this.#cache.get(href, () => this.#resolve(target)),
error => getGitHubLookupFailurePresentationForTarget(target, error),
[context.onDidRequestRefresh],
{ event: this.#onDidChangeAuthentication.event, presentation: loadingPresentation },
);
}
dispose(): void {
this.#authenticationSubscription.dispose();
this.#onDidChangeAuthentication.dispose();
}
async #resolve(target: GitHubTarget): Promise<LinkPresentation> {
const request = new GitHubRequest(await this.#getAccessToken());
switch (target.kind) {
case 'issue': {
const issue = await request.get(`/repos/${target.owner}/${target.repository}/issues/${target.number}`, readIssue);
const state = getGitHubIssueStatus(issue.state, issue.stateReason);
return {
kind: 'issue',
title: issue.title,
reference: `#${target.number}`,
status: state,
tooltip: `${target.owner}/${target.repository}#${target.number} · ${state.label}`,
ariaLabel: `Issue ${target.owner} slash ${target.repository} number ${target.number}, ${state.label}: ${issue.title}`,
};
}
case 'pullRequest': {
const pullRequest = await request.get(`/repos/${target.owner}/${target.repository}/pulls/${target.number}`, readPullRequest);
const state = getGitHubPullRequestStatus(pullRequest.state, pullRequest.draft, pullRequest.merged);
const checksStatus = shouldShowGitHubPullRequestChecks(state)
? checkRunStatus(await request.getAll(
`/repos/${target.owner}/${target.repository}/commits/${encodeURIComponent(pullRequest.headSha)}/check-runs`,
readCheckRuns,
))
: undefined;
return {
kind: 'pullRequest',
title: pullRequest.title,
reference: `#${target.number}`,
status: state,
...(checksStatus ? { secondaryStatus: checksStatus } : {}),
tooltip: [target.owner + '/' + target.repository + '#' + target.number, state.label, checksStatus?.label].filter(Boolean).join(' · '),
ariaLabel: `Pull request ${target.owner} slash ${target.repository} number ${target.number}, ${state.label}${checksStatus ? `, ${checksStatus.label}` : ''}: ${pullRequest.title}`,
};
}
case 'repository': {
const repository = await request.get(`/repos/${target.owner}/${target.repository}`, readRepository);
const details = [
repository.language,
repository.stars === undefined ? undefined : `${formatCount(repository.stars)} stars`,
].filter((value): value is string => !!value);
return {
kind: 'repository',
...(details.length ? { detail: details.join(' · ') } : {}),
tooltip: `${target.owner}/${target.repository}`,
ariaLabel: `GitHub repository ${target.owner} slash ${target.repository}`,
};
}
case 'tree': {
const refs = await request.get(
`/repos/${target.owner}/${target.repository}/git/matching-refs/heads/${encodeURIComponent(target.segments[0])}`,
readGitRefs,
);
const tree = resolveGitHubTreePath(target.segments, refs);
if (!tree) {
throw new GitHubLookupError('notFound', `GitHub did not find branch ${target.segments.join('/')}.`);
}
if (tree.path) {
return {
kind: 'folder',
detail: `${target.owner}/${target.repository} · ${tree.path}`,
tooltip: target.href,
ariaLabel: `Folder ${tree.path} in ${target.owner} slash ${target.repository}`,
};
}
const branch = await request.get(
`/repos/${target.owner}/${target.repository}/branches/${encodeURIComponent(tree.branch)}`,
readBranch,
);
return {
kind: 'branch',
detail: branch.sha.slice(0, 7),
tooltip: `${target.owner}/${target.repository} · ${tree.branch}`,
ariaLabel: `Branch ${tree.branch} in ${target.owner} slash ${target.repository}`,
};
}
case 'file':
return {
kind: 'file',
detail: `${target.owner}/${target.repository} · ${target.path}`,
tooltip: target.href,
ariaLabel: `File ${target.path} in ${target.owner} slash ${target.repository}`,
};
}
}
#getAccessToken(): Promise<string> {
if (this.#accessToken) {
return this.#accessToken;
}
const value = this.#readAccessToken().catch(error => {
if (
this.#accessToken === value
&& !(error instanceof GitHubLookupError && error.kind === 'authenticationRequired')
) {
this.#accessToken = undefined;
}
throw error;
});
this.#accessToken = value;
return value;
}
async #readAccessToken(): Promise<string> {
try {
const accounts = await vscode.authentication.getAccounts('github');
for (const account of accounts) {
const session = await vscode.authentication.getSession('github', [], { silent: true, account });
if (session?.scopes.includes(githubRepositoryScope)) {
return session.accessToken;
}
}
const session = await vscode.authentication.getSession('github', [githubRepositoryScope], {
createIfNone: {
detail: 'The Markdown editor needs repository access to show issue, pull request, and CI status.',
},
...(accounts.length === 1 ? { account: accounts[0] } : {}),
});
return session.accessToken;
} catch (error) {
throw new GitHubLookupError(
'authenticationRequired',
`GitHub repository access was not authorized.${error instanceof Error ? ` ${error.message}` : ''}`,
);
}
}
}
class GitHubRequest {
readonly #accessToken: string;
constructor(accessToken: string) {
this.#accessToken = accessToken;
}
async get<T>(apiPath: string, read: (value: unknown) => T | undefined): Promise<T> {
const response = await fetch(`https://api.github.com${apiPath}`, {
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${this.#accessToken}`,
},
});
if (!response.ok) {
throw GitHubLookupError.fromResponse(apiPath, response);
}
const value = read(await response.json());
if (!value) {
throw new GitHubLookupError(
'invalidResponse',
`GitHub request ${apiPath} returned an unexpected response.`,
);
}
return value;
}
async getAll<T>(apiPath: string, read: (value: unknown) => readonly T[] | undefined): Promise<readonly T[]> {
const values: T[] = [];
const pageSize = 100;
for (let page = 1; ; page++) {
const separator = apiPath.includes('?') ? '&' : '?';
const pageValues = await this.get(`${apiPath}${separator}per_page=${pageSize}&page=${page}`, read);
values.push(...pageValues);
if (pageValues.length < pageSize) {
return values;
}
}
}
}
type GitHubLookupFailureKind =
| 'authenticationRequired'
| 'authenticationFailed'
| 'accessDenied'
| 'rateLimited'
| 'notFound'
| 'invalidResponse'
| 'requestFailed';
export class GitHubLookupError extends Error {
readonly kind: GitHubLookupFailureKind;
constructor(kind: GitHubLookupFailureKind, message: string) {
super(message);
this.name = 'GitHubLookupError';
this.kind = kind;
}
static fromResponse(apiPath: string, response: Response): GitHubLookupError {
const message = `GitHub request ${apiPath} failed: ${response.status} ${response.statusText}`;
if (response.status === 401) {
return new GitHubLookupError('authenticationFailed', message);
}
if (response.status === 403) {
return new GitHubLookupError(
response.headers.get('x-ratelimit-remaining') === '0' ? 'rateLimited' : 'accessDenied',
message,
);
}
if (response.status === 404) {
return new GitHubLookupError('notFound', message);
}
return new GitHubLookupError('requestFailed', message);
}
}
type GitHubTarget =
| { readonly kind: 'issue'; readonly href: string; readonly owner: string; readonly repository: string; readonly number: number }
| { readonly kind: 'pullRequest'; readonly href: string; readonly owner: string; readonly repository: string; readonly number: number }
| { readonly kind: 'repository'; readonly href: string; readonly owner: string; readonly repository: string }
| { readonly kind: 'tree'; readonly href: string; readonly owner: string; readonly repository: string; readonly segments: readonly [string, ...string[]] }
| { readonly kind: 'file'; readonly href: string; readonly owner: string; readonly repository: string; readonly path: string };
function parseGitHubTarget(href: string): GitHubTarget | undefined {
let uri: URL;
try {
uri = new URL(href);
} catch {
return undefined;
}
if (uri.protocol !== 'https:' || uri.hostname.toLowerCase() !== 'github.com') {
return undefined;
}
const segments = decodeUrlPathSegments(uri);
if (!segments) {
return undefined;
}
const [owner, repository, category, identifier, ...rest] = segments;
if (!owner || !repository) {
return undefined;
}
if (!category) {
return { kind: 'repository', href, owner, repository };
}
if (category === 'issues' || category === 'pull') {
const number = Number(identifier);
return Number.isInteger(number) && number > 0
? { kind: category === 'issues' ? 'issue' : 'pullRequest', href, owner, repository, number }
: undefined;
}
if (category === 'tree' && identifier) {
return { kind: 'tree', href, owner, repository, segments: [identifier, ...rest] };
}
if (category === 'blob' && identifier && rest.length) {
return { kind: 'file', href, owner, repository, path: rest.join('/') };
}
return undefined;
}
export function getGitHubLookupFailurePresentation(
href: string,
error: unknown,
): LinkPresentation | undefined {
const target = parseGitHubTarget(href);
if (!target) {
return undefined;
}
return getGitHubLookupFailurePresentationForTarget(target, error);
}
function getGitHubLookupFailurePresentationForTarget(
target: GitHubTarget,
error: unknown,
): LinkPresentation {
const failure = error instanceof GitHubLookupError
? githubLookupFailureDescription(error.kind)
: { label: 'Lookup failed', detail: 'The GitHub request could not be completed.' };
const kind = target.kind === 'tree' ? 'resource' : target.kind;
return {
kind,
status: { kind: 'error', label: failure.label },
tooltip: `${failure.detail} ${error instanceof Error ? error.message : ''}`.trim(),
ariaLabel: `GitHub ${kind} lookup failed: ${failure.label}`,
};
}
function githubLookupFailureDescription(kind: GitHubLookupFailureKind): {
readonly label: string;
readonly detail: string;
} {
switch (kind) {
case 'authenticationRequired':
return {
label: 'Authorization required',
detail: 'Authorize GitHub repository access in VS Code to load this link.',
};
case 'authenticationFailed':
return {
label: 'Authentication failed',
detail: 'GitHub rejected the current VS Code authentication session.',
};
case 'accessDenied':
return {
label: 'Access denied',
detail: 'The current GitHub account cannot access this resource.',
};
case 'rateLimited':
return {
label: 'Rate limited',
detail: 'GitHub API rate limiting prevented this lookup.',
};
case 'notFound':
return {
label: 'Not found',
detail: 'GitHub did not find this resource, or the current account cannot access it.',
};
case 'invalidResponse':
return {
label: 'Invalid response',
detail: 'GitHub returned data the Markdown editor could not read.',
};
case 'requestFailed':
return {
label: 'Lookup failed',
detail: 'GitHub returned an unsuccessful response.',
};
}
}
interface GitRefData {
readonly ref: string;
}
function readGitRefs(value: unknown): readonly GitRefData[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const refs: GitRefData[] = [];
for (const item of value) {
if (!isRecord(item) || typeof item.ref !== 'string') {
return undefined;
}
refs.push({ ref: item.ref });
}
return refs;
}
export function resolveGitHubTreePath(
segments: readonly string[],
refs: readonly GitRefData[],
): { readonly branch: string; readonly path?: string } | undefined {
const target = segments.join('/');
const branch = refs
.map(ref => ref.ref.startsWith('refs/heads/') ? ref.ref.slice('refs/heads/'.length) : undefined)
.filter((candidate): candidate is string => !!candidate && (target === candidate || target.startsWith(`${candidate}/`)))
.sort((a, b) => b.length - a.length)[0];
if (!branch) {
return undefined;
}
const path = target === branch ? undefined : target.slice(branch.length + 1);
return { branch, ...(path ? { path } : {}) };
}
interface IssueData {
readonly title: string;
readonly state: 'open' | 'closed';
readonly stateReason?: 'completed' | 'not_planned' | 'reopened';
}
function readIssue(value: unknown): IssueData | undefined {
if (!isRecord(value) || typeof value.title !== 'string' || (value.state !== 'open' && value.state !== 'closed')) {
return undefined;
}
const stateReason = value.state_reason === 'completed' || value.state_reason === 'not_planned' || value.state_reason === 'reopened'
? value.state_reason
: undefined;
return { title: value.title, state: value.state, ...(stateReason ? { stateReason } : {}) };
}
interface PullRequestData extends IssueData {
readonly draft: boolean;
readonly merged: boolean;
readonly headSha: string;
}
function readPullRequest(value: unknown): PullRequestData | undefined {
if (!isRecord(value) || !isRecord(value.head)) {
return undefined;
}
const issue = readIssue(value);
return issue
&& typeof value.draft === 'boolean'
&& typeof value.merged === 'boolean'
&& typeof value.head.sha === 'string'
? { ...issue, draft: value.draft, merged: value.merged, headSha: value.head.sha }
: undefined;
}
interface CheckRunData {
readonly status: string;
readonly conclusion: string | null;
}
function readCheckRuns(value: unknown): readonly CheckRunData[] | undefined {
if (!isRecord(value) || !Array.isArray(value.check_runs)) {
return undefined;
}
const runs: CheckRunData[] = [];
for (const run of value.check_runs) {
if (!isRecord(run) || typeof run.status !== 'string' || (run.conclusion !== null && typeof run.conclusion !== 'string')) {
return undefined;
}
runs.push({ status: run.status, conclusion: run.conclusion });
}
return runs;
}
export function getGitHubIssueStatus(
state: IssueData['state'],
stateReason: IssueData['stateReason'],
): LinkPresentationStatus {
if (state === 'open') {
return { kind: 'open', label: 'Open' };
}
return stateReason === 'not_planned'
? { kind: 'notPlanned', label: 'Not planned' }
: { kind: 'closed', label: 'Closed' };
}
export function getGitHubPullRequestStatus(
state: PullRequestData['state'],
draft: boolean,
merged: boolean,
): LinkPresentationStatus {
if (merged) {
return { kind: 'merged', label: 'Merged' };
}
if (draft) {
return { kind: 'draft', label: 'Draft' };
}
return state === 'closed'
? { kind: 'closed', label: 'Closed' }
: { kind: 'open', label: 'Open' };
}
export function shouldShowGitHubPullRequestChecks(status: LinkPresentationStatus): boolean {
return status.kind === 'open' || status.kind === 'draft';
}
function checkRunStatus(checks: readonly CheckRunData[] | undefined): LinkPresentationStatus | undefined {
if (!checks?.length) {
return undefined;
}
if (checks.some(check => check.status !== 'completed')) {
return { kind: 'pending', label: 'Checks running' };
}
if (checks.some(check => check.conclusion === 'failure'
|| check.conclusion === 'timed_out'
|| check.conclusion === 'cancelled'
|| check.conclusion === 'action_required')) {
return { kind: 'error', label: 'Checks failed' };
}
return { kind: 'success', label: 'Checks passed' };
}
interface RepositoryData {
readonly language?: string;
readonly stars?: number;
}
function readRepository(value: unknown): RepositoryData | undefined {
if (!isRecord(value)) {
return undefined;
}
return {
...(typeof value.language === 'string' ? { language: value.language } : {}),
...(typeof value.stargazers_count === 'number' ? { stars: value.stargazers_count } : {}),
};
}
interface BranchData {
readonly sha: string;
}
function readBranch(value: unknown): BranchData | undefined {
return isRecord(value)
&& isRecord(value.commit)
&& typeof value.commit.sha === 'string'
? { sha: value.commit.sha }
: undefined;
}
function formatCount(value: number): string {
return value >= 1000 ? `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k` : String(value);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
@@ -0,0 +1,152 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export type LinkPresentationKind =
| 'resource'
| 'issue'
| 'pullRequest'
| 'commit'
| 'file'
| 'folder'
| 'session'
| 'repository'
| 'branch';
export type LinkPresentationStatusKind =
| 'neutral'
| 'pending'
| 'success'
| 'warning'
| 'error'
| 'open'
| 'closed'
| 'merged'
| 'draft'
| 'notPlanned';
export interface LinkPresentationStatus {
readonly kind: LinkPresentationStatusKind;
readonly label: string;
}
export interface LinkPresentation {
readonly kind: LinkPresentationKind;
readonly title?: string;
readonly detail?: string;
readonly reference?: string;
readonly status?: LinkPresentationStatus;
readonly secondaryStatus?: LinkPresentationStatus;
readonly tooltip?: string;
readonly ariaLabel?: string;
readonly isLoading?: boolean;
}
export interface GitCommitPresentationData {
readonly hash: string;
readonly message: string;
readonly shortStat?: {
readonly insertions: number;
readonly deletions: number;
};
}
export function buildGitCommitPresentation(commit: GitCommitPresentationData): LinkPresentation {
const title = commit.message.split(/\r?\n/, 1)[0];
const insertions = commit.shortStat?.insertions ?? 0;
const deletions = commit.shortStat?.deletions ?? 0;
const shortHash = commit.hash.slice(0, 7);
return {
kind: 'commit',
detail: title,
tooltip: `${shortHash} · ${title} · ${insertions} insertions, ${deletions} deletions`,
ariaLabel: `Commit ${shortHash}, ${insertions} insertions and ${deletions} deletions: ${title}`,
};
}
export function buildGitCommitLookupFailurePresentation(shortHash: string, tooltip: string): LinkPresentation {
return {
kind: 'commit',
status: { kind: 'error', label: 'Not available' },
tooltip,
ariaLabel: `Git commit ${shortHash} could not be resolved`,
};
}
export interface WorkspaceRepositoryPresentationData {
readonly label: string;
readonly href: string;
readonly branch?: string;
readonly changeCount: number;
}
export function buildWorkspaceRepositoryPresentation(data: WorkspaceRepositoryPresentationData): LinkPresentation {
const detail = [data.branch, data.changeCount ? `${data.changeCount} changes` : 'clean'].filter((value): value is string => !!value).join(' · ');
return {
kind: 'repository',
...(detail ? { detail } : {}),
status: data.branch ? { kind: data.changeCount ? 'warning' : 'success', label: data.branch } : undefined,
tooltip: data.href,
ariaLabel: `Local repository ${data.label}${data.branch ? ` on branch ${data.branch}` : ''}, ${data.changeCount ? `${data.changeCount} changes` : 'clean'}`,
};
}
export interface WorkspaceResourcePresentationData {
readonly kind: 'file' | 'folder';
readonly label: string;
readonly href: string;
readonly branch?: string;
readonly modified: boolean;
}
export function buildWorkspaceResourcePresentation(data: WorkspaceResourcePresentationData): LinkPresentation {
const details = [
compactParent(data.label),
data.branch,
data.modified ? 'modified' : undefined,
].filter((value): value is string => !!value);
return {
kind: data.kind,
...(details.length ? { detail: details.join(' · ') } : {}),
tooltip: data.href,
ariaLabel: `${data.kind === 'folder' ? 'Folder' : 'File'} ${data.label}`,
};
}
export function buildLoadingLinkPresentation(kind: LinkPresentation['kind'], label = 'Loading'): LinkPresentation {
return {
kind,
status: { kind: 'pending', label },
};
}
export function buildWorkspaceLookupFailurePresentation(
kind: 'file' | 'folder',
label: string,
tooltip: string,
ariaLabel: string,
): LinkPresentation {
return {
kind,
status: { kind: 'error', label },
tooltip,
ariaLabel,
};
}
function relativeParent(value: string): string | undefined {
const separator = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
return separator > 0 ? value.slice(0, separator) : undefined;
}
function compactParent(value: string): string | undefined {
const parent = relativeParent(value);
if (!parent) {
return undefined;
}
if (!/^(?:[a-z]:[\\/]|[\\/])/i.test(parent)) {
return parent;
}
return parent.split(/[\\/]+/).filter(Boolean).slice(-4).join('/');
}
@@ -8,11 +8,6 @@ import { derived, observableValue, type IObservable, type ISettableObservable }
import * as vscode from 'vscode';
import type { ILogger } from '../../logging';
const cacheLifetimeMs = 60_000;
const persistentCacheLifetimeMs = 7 * 24 * 60 * 60 * 1_000;
const persistentCacheEntryLimit = 100;
const persistentCacheKey = 'markdown.linkPresentations.cache.v1';
export type LinkPresentation = MarkdownLinkPresentation & {
readonly isLoading?: boolean;
};
@@ -37,118 +32,6 @@ export function decodeUrlPathSegments(uri: URL): string[] | undefined {
}
}
interface LinkPresentationCacheEntry {
readonly value: Promise<LinkPresentation>;
readonly expiresAt: number;
}
interface PersistedLinkPresentationCacheEntry {
readonly href: string;
readonly presentation: LinkPresentation;
readonly storedAt: number;
}
export class LinkPresentationCache {
readonly #entries = new Map<string, LinkPresentationCacheEntry>();
readonly #persistentEntries = new Map<string, PersistedLinkPresentationCacheEntry>();
readonly #storage: vscode.Memento | undefined;
readonly #logger: ILogger | undefined;
#writeQueue = Promise.resolve();
#generation = 0;
constructor(storage?: vscode.Memento, logger?: ILogger) {
this.#storage = storage;
this.#logger = logger;
for (const entry of readPersistentLinkPresentationCache(storage?.get(persistentCacheKey))) {
this.#persistentEntries.set(entry.href, entry);
}
}
getPersisted(href: string, now = Date.now()): LinkPresentation | undefined {
const entry = this.#persistentEntries.get(href);
if (!entry) {
return undefined;
}
if (entry.storedAt + persistentCacheLifetimeMs <= now) {
this.#persistentEntries.delete(href);
this.#persist();
return undefined;
}
return { ...entry.presentation, isLoading: true };
}
get(
href: string,
resolve: () => Promise<LinkPresentation>,
now = Date.now(),
): Promise<LinkPresentation> {
this.#removeExpiredMemoryEntries(now);
const cached = this.#entries.get(href);
if (cached) {
return cached.value;
}
const value = resolve();
const entry = { value, expiresAt: now + cacheLifetimeMs };
const generation = this.#generation;
this.#entries.set(href, entry);
void value.then(presentation => {
if (generation !== this.#generation) {
return;
}
this.#persistentEntries.set(href, {
href,
presentation: { ...presentation, isLoading: undefined },
storedAt: Date.now(),
});
this.#trimPersistentEntries();
this.#persist();
}, () => {
if (this.#entries.get(href) === entry) {
this.#entries.delete(href);
}
});
return value;
}
clear(): void {
this.#generation++;
this.#entries.clear();
this.#persistentEntries.clear();
this.#persist();
}
#removeExpiredMemoryEntries(now: number): void {
for (const [key, entry] of this.#entries) {
if (entry.expiresAt <= now) {
this.#entries.delete(key);
}
}
}
#trimPersistentEntries(): void {
const entries = [...this.#persistentEntries.values()].sort((a, b) => b.storedAt - a.storedAt);
this.#persistentEntries.clear();
for (const entry of entries.slice(0, persistentCacheEntryLimit)) {
this.#persistentEntries.set(entry.href, entry);
}
}
#persist(): void {
const storage = this.#storage;
if (!storage) {
return;
}
const value = {
version: 1,
entries: [...this.#persistentEntries.values()],
};
this.#writeQueue = this.#writeQueue.then(() => storage.update(persistentCacheKey, value)).then(undefined, error => {
this.#logger?.trace('Markdown rich link', 'Failed to persist link presentation cache', error);
});
}
}
export class ImmutableLinkPresentationCache {
readonly #entries = new Map<string, Promise<LinkPresentation>>();
@@ -243,72 +126,3 @@ class AsyncLinkPresentation implements vscode.Disposable {
});
}
}
function readPersistentLinkPresentationCache(value: unknown): readonly PersistedLinkPresentationCacheEntry[] {
if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.entries)) {
return [];
}
return value.entries.flatMap(entry => {
if (!isRecord(entry) || typeof entry.href !== 'string' || typeof entry.storedAt !== 'number') {
return [];
}
const presentation = readLinkPresentation(entry.presentation);
return presentation ? [{ href: entry.href, presentation, storedAt: entry.storedAt }] : [];
});
}
function readLinkPresentation(value: unknown): LinkPresentation | undefined {
if (!isRecord(value) || !isLinkPresentationKind(value.kind)) {
return undefined;
}
const status = readLinkPresentationStatus(value.status);
const secondaryStatus = readLinkPresentationStatus(value.secondaryStatus);
if ((value.status !== undefined && !status) || (value.secondaryStatus !== undefined && !secondaryStatus)) {
return undefined;
}
return {
kind: value.kind,
...(typeof value.title === 'string' ? { title: value.title } : {}),
...(typeof value.detail === 'string' ? { detail: value.detail } : {}),
...(typeof value.reference === 'string' ? { reference: value.reference } : {}),
...(status ? { status } : {}),
...(secondaryStatus ? { secondaryStatus } : {}),
...(typeof value.tooltip === 'string' ? { tooltip: value.tooltip } : {}),
...(typeof value.ariaLabel === 'string' ? { ariaLabel: value.ariaLabel } : {}),
};
}
function readLinkPresentationStatus(value: unknown): MarkdownLinkPresentation['status'] | undefined {
return isRecord(value) && isLinkPresentationStatusKind(value.kind) && typeof value.label === 'string'
? { kind: value.kind, label: value.label }
: undefined;
}
function isLinkPresentationKind(value: unknown): value is LinkPresentation['kind'] {
return value === 'resource'
|| value === 'issue'
|| value === 'pullRequest'
|| value === 'commit'
|| value === 'file'
|| value === 'folder'
|| value === 'session'
|| value === 'repository'
|| value === 'branch';
}
function isLinkPresentationStatusKind(value: unknown): value is NonNullable<MarkdownLinkPresentation['status']>['kind'] {
return value === 'neutral'
|| value === 'pending'
|| value === 'success'
|| value === 'warning'
|| value === 'error'
|| value === 'open'
|| value === 'closed'
|| value === 'merged'
|| value === 'draft'
|| value === 'notPlanned';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
@@ -7,13 +7,15 @@ import { autorun, type IObservable } from '@vscode/observables';
import * as vscode from 'vscode';
import type { ILogger } from '../../logging';
import { Disposable } from '../../util/dispose';
import { GitHubLinkPresentationResolver } from './githubLinkPresentationResolver';
import { GitLinkPresentationResolver } from './gitLinkPresentationResolver';
import { ImmutableLinkPresentationCache, LinkPresentationCache, type LinkPresentation, type LinkPresentationResolver, type LinkPresentationResolverContext } from './linkPresentationResolver';
import { ImmutableLinkPresentationCache, type LinkPresentation, type LinkPresentationResolver, type LinkPresentationResolverContext } from './linkPresentationResolver';
import { WorkspaceLinkPresentationResolver } from './workspaceLinkPresentationResolver';
const refreshIntervalMs = 30_000;
export const linkPresentationProviderId = 'markdown.linkPresentations';
export const linkPresentationProviderIds = [
'markdown.gitCommitLinkPresentations',
'markdown.workspaceFileLinkPresentations',
] as const;
export interface LinkPresentationWatch extends vscode.Disposable {
readonly presentation: IObservable<LinkPresentation>;
@@ -138,28 +140,25 @@ export class LinkPresentationService extends Disposable {
}
}
export function createSharedLinkPresentationService(globalState: vscode.Memento, logger: ILogger): LinkPresentationService {
export function createSharedLinkPresentationService(logger: ILogger): LinkPresentationService {
return new LinkPresentationService([
new GitLinkPresentationResolver(new ImmutableLinkPresentationCache()),
new GitHubLinkPresentationResolver(new LinkPresentationCache(globalState, logger)),
new WorkspaceLinkPresentationResolver(),
], logger);
}
export function registerLinkPresentationProvider(service: LinkPresentationService): vscode.Disposable {
return vscode.window.registerLinkPresentationProvider(
linkPresentationProviderId,
{
provideLinkPresentationWatcher: resource => {
const href = resource.toString(true);
const watch = service.watch(href);
if (!watch) {
throw new Error(`No link presentation resolver accepted ${href}.`);
}
return new ExtensionLinkPresentationWatcher(watch);
},
const provider: vscode.LinkPresentationProvider = {
provideLinkPresentationWatcher: resource => {
const href = resource.toString(true);
const watch = service.watch(href);
if (!watch) {
throw new Error(`No link presentation resolver accepted ${href}.`);
}
return new ExtensionLinkPresentationWatcher(watch);
},
);
};
return vscode.Disposable.from(...linkPresentationProviderIds.map(id => vscode.window.registerLinkPresentationProvider(id, provider)));
}
class ExtensionLinkPresentationWatcher extends Disposable implements vscode.LinkPresentationWatcher {
@@ -6,6 +6,7 @@
import type { LinkPresentation } from '@vscode/markdown-editor';
import type { IObservable } from '@vscode/observables';
import * as vscode from 'vscode';
import { buildLoadingLinkPresentation, buildWorkspaceLookupFailurePresentation, buildWorkspaceResourcePresentation } from './linkPresentationBuilders';
import { createAsyncLinkPresentation, type LinkPresentationResolver, type LinkPresentationResolverContext } from './linkPresentationResolver';
export class WorkspaceLinkPresentationResolver implements LinkPresentationResolver {
@@ -30,18 +31,15 @@ export class WorkspaceLinkPresentationResolver implements LinkPresentationResolv
}
return createAsyncLinkPresentation(
href,
{
kind: 'file',
status: { kind: 'pending', label: vscode.l10n.t("Loading") },
},
buildLoadingLinkPresentation('file', vscode.l10n.t("Loading")),
context,
() => this.#resolve(href),
error => ({
kind: 'file',
status: { kind: 'error', label: vscode.l10n.t("Not found") },
tooltip: error instanceof Error ? error.message : vscode.l10n.t("The workspace resource could not be resolved."),
ariaLabel: vscode.l10n.t("Workspace resource could not be resolved: {0}", href),
}),
error => buildWorkspaceLookupFailurePresentation(
'file',
vscode.l10n.t("Not found"),
error instanceof Error ? error.message : vscode.l10n.t("The workspace resource could not be resolved."),
vscode.l10n.t("Workspace resource could not be resolved: {0}", href),
),
[context.onDidRequestRefresh, this.#onDidChangeWorkspaceResource.event],
);
}
@@ -53,38 +51,17 @@ export class WorkspaceLinkPresentationResolver implements LinkPresentationResolv
async #resolve(href: string): Promise<LinkPresentation> {
const uri = vscode.Uri.parse(href);
const stat = await vscode.workspace.fs.stat(uri);
return this.#present(uri, stat.type === vscode.FileType.Directory ? 'folder' : 'file');
}
async #present(uri: vscode.Uri, kind: 'file' | 'folder'): Promise<LinkPresentation> {
await vscode.workspace.fs.stat(uri);
const label = vscode.workspace.asRelativePath(uri, false);
const repository = await this.#getGitApi().then(api => api?.getRepository(uri) ?? undefined);
const branch = repository?.state.HEAD?.name;
const changed = repository ? repositoryChangeCount(repository) : 0;
const isRepositoryRoot = kind === 'folder' && repository?.rootUri.fsPath === uri.fsPath;
if (isRepositoryRoot) {
const detail = [branch, changed ? `${changed} changes` : 'clean'].filter((value): value is string => !!value).join(' · ');
return {
kind: 'repository',
...(detail ? { detail } : {}),
status: branch ? { kind: changed ? 'warning' : 'success', label: branch } : undefined,
tooltip: uri.toString(true),
ariaLabel: `Local repository ${label}${branch ? ` on branch ${branch}` : ''}, ${changed ? `${changed} changes` : 'clean'}`,
};
}
const details = [
compactParent(label),
return buildWorkspaceResourcePresentation({
kind: 'file',
label,
href: uri.toString(true),
branch,
repository && repositoryContainsChange(repository, uri) ? 'modified' : undefined,
].filter((value): value is string => !!value);
return {
kind,
...(details.length ? { detail: details.join(' · ') } : {}),
tooltip: uri.toString(true),
ariaLabel: `${kind === 'folder' ? 'Folder' : 'File'} ${label}`,
};
modified: !!repository && repositoryContainsChange(repository, uri),
});
}
#getGitApi(): Promise<GitApi | undefined> {
@@ -125,14 +102,6 @@ interface GitChange {
readonly uri: vscode.Uri;
}
function repositoryChangeCount(repository: GitRepository): number {
const state = repository.state;
return state.mergeChanges.length
+ state.indexChanges.length
+ state.workingTreeChanges.length
+ state.untrackedChanges.length;
}
function repositoryContainsChange(repository: GitRepository, uri: vscode.Uri): boolean {
const key = uri.toString();
const state = repository.state;
@@ -143,19 +112,3 @@ function repositoryContainsChange(repository: GitRepository, uri: vscode.Uri): b
...state.untrackedChanges,
].some(change => change.uri.toString() === key);
}
function relativeParent(value: string): string | undefined {
const separator = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
return separator > 0 ? value.slice(0, separator) : undefined;
}
function compactParent(value: string): string | undefined {
const parent = relativeParent(value);
if (!parent) {
return undefined;
}
if (!/^(?:[a-z]:[\\/]|[\\/])/i.test(parent)) {
return parent;
}
return parent.split(/[\\/]+/).filter(Boolean).slice(-4).join('/');
}
@@ -761,12 +761,12 @@ export class MarkdownEditorProvider extends Disposable implements vscode.CustomT
content: document.getText(),
documentVersion: document.version,
readonly: this.#globalState.get(MarkdownEditorProvider.#readonlyStateKey, true),
richLinksEnabled: vscode.workspace.getConfiguration('markdown').get<boolean>('experimental.richLinks.enabled', false),
richLinksEnabled: vscode.workspace.getConfiguration('markdown').get<boolean>('experimental.richLinks.enabled', true),
linkPresentationRules: vscode.window.linkPresentationRules.map(rule => ({
id: rule.id,
source: rule.uriPattern.source,
flags: rule.uriPattern.flags,
initialKind: rule.initialKind === 'chat' ? 'session' : rule.initialKind,
kind: rule.kind === 'chat' ? 'session' : rule.kind,
})),
});
@@ -8,7 +8,7 @@ export interface MarkdownEditorInitialState {
readonly documentVersion: number;
readonly readonly: boolean;
readonly richLinksEnabled: boolean;
readonly linkPresentationRules: readonly { id: string; source: string; flags: string; initialKind: string }[];
readonly linkPresentationRules: readonly { id: string; source: string; flags: string; kind: string }[];
}
/**
@@ -7,112 +7,11 @@ import * as assert from 'assert';
import { autorun, derived, observableValue } from '@vscode/observables';
import 'mocha';
import * as vscode from 'vscode';
import {
getGitHubIssueStatus,
getGitHubLookupFailurePresentation,
getGitHubPullRequestStatus,
GitHubLookupError,
resolveGitHubTreePath,
shouldShowGitHubPullRequestChecks,
} from '../preview/linkPresentation/githubLinkPresentationResolver';
import { getGitCommitPresentation, GitLinkPresentationResolver, normalizeGitRemoteUrl } from '../preview/linkPresentation/gitLinkPresentationResolver';
import { createAsyncLinkPresentation, ImmutableLinkPresentationCache, LinkPresentationCache } from '../preview/linkPresentation/linkPresentationResolver';
import { createAsyncLinkPresentation, ImmutableLinkPresentationCache } from '../preview/linkPresentation/linkPresentationResolver';
import { LinkPresentationService } from '../preview/linkPresentation/linkPresentationService';
class TestMemento implements vscode.Memento {
readonly #values = new Map<string, unknown>();
keys(): readonly string[] {
return [...this.#values.keys()];
}
get<T>(key: string): T | undefined;
get<T>(key: string, defaultValue: T): T;
get<T>(key: string, defaultValue?: T): T | undefined {
return (this.#values.get(key) as T | undefined) ?? defaultValue;
}
update(key: string, value: unknown): Thenable<void> {
if (value === undefined) {
this.#values.delete(key);
} else {
this.#values.set(key, value);
}
return Promise.resolve();
}
}
suite('Markdown editor rich links', () => {
test('separates GitHub branch names from folder paths', () => {
const refs = [
{ ref: 'refs/heads/main' },
{ ref: 'refs/heads/feature/rich-links' },
];
assert.deepStrictEqual(resolveGitHubTreePath(['main'], refs), {
branch: 'main',
});
assert.deepStrictEqual(resolveGitHubTreePath(['main', 'src', 'vs'], refs), {
branch: 'main',
path: 'src/vs',
});
assert.deepStrictEqual(resolveGitHubTreePath(['feature', 'rich-links'], refs), {
branch: 'feature/rich-links',
});
assert.deepStrictEqual(resolveGitHubTreePath(['feature', 'rich-links', 'src'], refs), {
branch: 'feature/rich-links',
path: 'src',
});
});
test('maps GitHub issue lifecycle states', () => {
assert.deepStrictEqual(getGitHubIssueStatus('open', undefined), { kind: 'open', label: 'Open' });
assert.deepStrictEqual(getGitHubIssueStatus('closed', 'completed'), { kind: 'closed', label: 'Closed' });
assert.deepStrictEqual(getGitHubIssueStatus('closed', 'not_planned'), { kind: 'notPlanned', label: 'Not planned' });
});
test('maps GitHub pull request lifecycle states', () => {
const open = getGitHubPullRequestStatus('open', false, false);
const draft = getGitHubPullRequestStatus('open', true, false);
const closed = getGitHubPullRequestStatus('closed', false, false);
const merged = getGitHubPullRequestStatus('closed', false, true);
assert.deepStrictEqual(open, { kind: 'open', label: 'Open' });
assert.deepStrictEqual(draft, { kind: 'draft', label: 'Draft' });
assert.deepStrictEqual(closed, { kind: 'closed', label: 'Closed' });
assert.deepStrictEqual(merged, { kind: 'merged', label: 'Merged' });
assert.strictEqual(shouldShowGitHubPullRequestChecks(open), true);
assert.strictEqual(shouldShowGitHubPullRequestChecks(draft), true);
assert.strictEqual(shouldShowGitHubPullRequestChecks(closed), false);
assert.strictEqual(shouldShowGitHubPullRequestChecks(merged), false);
});
test('keeps GitHub lookup failures visible and actionable', () => {
assert.deepStrictEqual(
getGitHubLookupFailurePresentation(
'https://github.com/hediet/demo-json-schema-validator/pull/5',
new GitHubLookupError('authenticationRequired', 'No GitHub session.'),
),
{
kind: 'pullRequest',
status: { kind: 'error', label: 'Authorization required' },
tooltip: 'Authorize GitHub repository access in VS Code to load this link. No GitHub session.',
ariaLabel: 'GitHub pullRequest lookup failed: Authorization required',
},
);
assert.deepStrictEqual(
getGitHubLookupFailurePresentation(
'https://github.com/hediet/demo-json-schema-validator/issues/1',
new GitHubLookupError('rateLimited', '403 Forbidden'),
)?.status,
{ kind: 'error', label: 'Rate limited' },
);
assert.strictEqual(
getGitHubLookupFailurePresentation('https://example.com/issues/1', new Error('offline')),
undefined,
);
});
test('normalizes common Git remote URL formats', () => {
assert.deepStrictEqual([
normalizeGitRemoteUrl('https://github.com/microsoft/vscode.git'),
@@ -156,13 +55,6 @@ suite('Markdown editor rich links', () => {
}
});
test('ignores malformed GitHub paths', () => {
assert.strictEqual(
getGitHubLookupFailurePresentation('https://github.com/microsoft/vscode/issues/%', new Error('failed')),
undefined,
);
});
test('shows Git commit metadata', () => {
assert.deepStrictEqual(getGitCommitPresentation({
hash: '1234567890abcdef',
@@ -225,8 +117,8 @@ suite('Markdown editor rich links', () => {
};
const service = new LinkPresentationService([resolver], { trace: () => { } });
const first = service.watch('https://github.com/microsoft/vscode/pull/1')!;
const second = service.watch('https://github.com/microsoft/vscode/pull/1')!;
const first = service.watch('https://example.com/pull/1')!;
const second = service.watch('https://example.com/pull/1')!;
assert.deepStrictEqual({ resolveCount, activeSubscriptions }, { resolveCount: 1, activeSubscriptions: 1 });
first.dispose();
@@ -234,7 +126,7 @@ suite('Markdown editor rich links', () => {
second.dispose();
assert.strictEqual(activeSubscriptions, 0);
const third = service.watch('https://github.com/microsoft/vscode/pull/1')!;
const third = service.watch('https://example.com/pull/1')!;
assert.deepStrictEqual({ resolveCount, activeSubscriptions }, { resolveCount: 2, activeSubscriptions: 1 });
third.dispose();
service.dispose();
@@ -286,65 +178,12 @@ suite('Markdown editor rich links', () => {
});
});
test('expires mutable link presentations after one minute', async () => {
const cache = new LinkPresentationCache();
let resolveCount = 0;
const resolve = async () => ({ kind: 'issue' as const, title: String(++resolveCount) });
const first = await cache.get('https://github.com/microsoft/vscode/issues/1', resolve, 0);
const cached = await cache.get('https://github.com/microsoft/vscode/issues/1', resolve, 59_999);
const refreshed = await cache.get('https://github.com/microsoft/vscode/issues/1', resolve, 60_000);
assert.deepStrictEqual({ first, cached, refreshed, resolveCount }, {
first: { kind: 'issue', title: '1' },
cached: { kind: 'issue', title: '1' },
refreshed: { kind: 'issue', title: '2' },
resolveCount: 2,
});
});
test('restores persistent presentations as loading and refreshes them', async () => {
const storage = new TestMemento();
const href = 'https://github.com/microsoft/vscode/issues/1';
let resolveCount = 0;
const resolve = async () => ({ kind: 'issue' as const, title: String(++resolveCount) });
const firstCache = new LinkPresentationCache(storage);
await firstCache.get(href, resolve);
await Promise.resolve();
const restoredCache = new LinkPresentationCache(storage);
const loading = restoredCache.getPersisted(href);
const refreshed = await restoredCache.get(href, resolve);
assert.deepStrictEqual({ loading, refreshed, resolveCount }, {
loading: { kind: 'issue', title: '1', isLoading: true },
refreshed: { kind: 'issue', title: '2' },
resolveCount: 2,
});
});
test('does not restore a request that completes after the cache is cleared', async () => {
const storage = new TestMemento();
const cache = new LinkPresentationCache(storage);
const href = 'https://github.com/microsoft/vscode/issues/1';
let completeRequest!: (value: { kind: 'issue'; title: string }) => void;
const request = new Promise<{ kind: 'issue'; title: string }>(resolve => completeRequest = resolve);
const pending = cache.get(href, () => request);
cache.clear();
completeRequest({ kind: 'issue', title: 'Old issue' });
await pending;
await Promise.resolve();
assert.strictEqual(new LinkPresentationCache(storage).getPersisted(href), undefined);
});
test('keeps a restored presentation visible while loading in the background', async () => {
const requestRefresh = new vscode.EventEmitter<void>();
let completeRefresh!: (value: { kind: 'issue'; title: string }) => void;
const refresh = new Promise<{ kind: 'issue'; title: string }>(resolve => completeRefresh = resolve);
const presentation = createAsyncLinkPresentation(
'https://github.com/microsoft/vscode/issues/1',
'https://example.com/issues/1',
{ kind: 'issue', title: 'Cached issue', isLoading: true },
{
onDidRequestRefresh: requestRefresh.event,
@@ -373,7 +212,7 @@ suite('Markdown editor rich links', () => {
let resolveCount = 0;
let completeRefresh!: (value: { kind: 'issue'; title: string }) => void;
const presentation = createAsyncLinkPresentation(
'https://github.com/microsoft/vscode/issues/1',
'https://example.com/issues/1',
{ kind: 'issue', status: { kind: 'pending', label: 'Loading' } },
{
onDidRequestRefresh: requestRefresh.event,
@@ -23,6 +23,7 @@ const enum CharCode {
let outputChannel: vscode.OutputChannel;
const SLOWED_DOWN_CONNECTION_DELAY = 800;
const agentHostBridgeConnectionTokenEnvironmentVariable = 'VSCODE_AGENT_HOST_BRIDGE_CONNECTION_TOKEN';
function isExpectedSocketCloseError(error: NodeJS.ErrnoException): boolean {
return error.code === 'ECONNRESET' || error.code === 'EPIPE' || error.code === 'ECONNABORTED';
@@ -190,7 +191,7 @@ export function activate(context: vscode.ExtensionContext) {
}
const agentHostBridgeToken = getConfiguration('agentHostBridgeConnectionToken');
if (typeof agentHostBridgeToken === 'string' && agentHostBridgeToken) {
commandArgs.push('--agent-host-bridge-connection-token', agentHostBridgeToken);
env[agentHostBridgeConnectionTokenEnvironmentVariable] = agentHostBridgeToken;
}
if (!commit) { // dev mode
+81 -243
View File
@@ -12,8 +12,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.82.0",
"@devcontainers/cli": "0.88.0",
"@github/copilot": "1.0.81-0",
"@github/copilot-sdk": "1.0.11",
"@github/copilot": "1.0.81-12",
"@github/copilot-sdk": "1.0.13-preview.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/dev-tunnels-connections": "^1.3.41",
@@ -31,7 +31,7 @@
"@vscode/fs-copyfile": "2.0.0",
"@vscode/iconv-lite-umd": "0.7.1",
"@vscode/native-watchdog": "^1.4.6",
"@vscode/os-proxy-resolver": "^0.3.0",
"@vscode/os-proxy-resolver": "^0.4.0",
"@vscode/policy-watcher": "^1.4.0",
"@vscode/proxy-agent": "^0.44.0",
"@vscode/ripgrep-universal": "^1.18.0",
@@ -1155,9 +1155,9 @@
}
},
"node_modules/@github/copilot": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.81-0.tgz",
"integrity": "sha512-0JggnsNkKQl5O6ilPxrjxDahARG/kPPRzDQvEHnuUBisfJi6PSRc4BXNtH+cc704nifIuj8DTb16lK11sRbneA==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.81-12.tgz",
"integrity": "sha512-Gs0LB6j8C2D02oagnBaX+g8U03ulT3utomXnW9TrZWdtY4JVMRIu20cr/aMkr24xATX9jUbPbTfGJAwzP7N/gA==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"detect-libc": "^2.1.2"
@@ -1166,20 +1166,20 @@
"copilot": "npm-loader.js"
},
"optionalDependencies": {
"@github/copilot-darwin-arm64": "1.0.81-0",
"@github/copilot-darwin-x64": "1.0.81-0",
"@github/copilot-linux-arm64": "1.0.81-0",
"@github/copilot-linux-x64": "1.0.81-0",
"@github/copilot-linuxmusl-arm64": "1.0.81-0",
"@github/copilot-linuxmusl-x64": "1.0.81-0",
"@github/copilot-win32-arm64": "1.0.81-0",
"@github/copilot-win32-x64": "1.0.81-0"
"@github/copilot-darwin-arm64": "1.0.81-12",
"@github/copilot-darwin-x64": "1.0.81-12",
"@github/copilot-linux-arm64": "1.0.81-12",
"@github/copilot-linux-x64": "1.0.81-12",
"@github/copilot-linuxmusl-arm64": "1.0.81-12",
"@github/copilot-linuxmusl-x64": "1.0.81-12",
"@github/copilot-win32-arm64": "1.0.81-12",
"@github/copilot-win32-x64": "1.0.81-12"
}
},
"node_modules/@github/copilot-darwin-arm64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.81-0.tgz",
"integrity": "sha512-8KOUMQ0OdmNQXPhjsKpo86VL/ZUT/lN4329Kdk6cpsvkM3x4+PdFNIJ7j9+4O9rB1P3g5gJ04XtR+qAkmeTekg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.81-12.tgz",
"integrity": "sha512-oaJHeCJpvr9ClfrnhFou2AZUd7SqxGrdCkBnVM1tYduUDPzb9xqdFsMcJn2g0hfPM1yO3khjWU6WQxmuQCyJnA==",
"cpu": [
"arm64"
],
@@ -1193,9 +1193,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.81-0.tgz",
"integrity": "sha512-6jbuDK3zyyU7Iiwnx3Shhs6LcEm03/NCnZYEJFd4oj+g4OtyCziXlVOeIGP+fntuIltIP418Dhc6qd8LL45/Qg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.81-12.tgz",
"integrity": "sha512-lYyFpCEA/usTGJSKWx1X5U4a4DiJ9A1A7vXrE0uE+HjS/9eJE2NBR1z3fPUP0V8HL0JUqAsSdM6rPbFfiE1c+Q==",
"cpu": [
"x64"
],
@@ -1209,9 +1209,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.81-0.tgz",
"integrity": "sha512-UUkCH9iaVdCNHQPjEdSm2L5AZyaM0x0XbxuRuhqB8KidcOCOHp2+Qr8VZnkkXA43CuYQHHi3mSkUGNR1QsEuLg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.81-12.tgz",
"integrity": "sha512-5jV7xuoqGndoQFxeM06IuEe4oEhY03PBIYyghvFVptQWY7Cj/Q3CMogIUaIQ+ugjuV8oRgBpm1JLJmr5Cw4FHA==",
"cpu": [
"arm64"
],
@@ -1228,9 +1228,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.81-0.tgz",
"integrity": "sha512-bAnfkeTFQcUSuUuKFLzBI8n/wrRbel54cb0aZnSYQCejnOSuYSF4d/osCTLDzZwo7aU0zWYxC38jJ9eh4HkUfQ==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.81-12.tgz",
"integrity": "sha512-/P0SMTeHQoMlV7MX+GCvIkht2uXZkLroyFMGjum+pD8XG7AsN8aozjILd5+JFEr6uxyhqnPjSOWj/vjmzmMoDA==",
"cpu": [
"x64"
],
@@ -1247,9 +1247,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-arm64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.81-0.tgz",
"integrity": "sha512-4Iqz4VPC5kULTC9KbyFP1NB3kmfZc+Z5vKSGhP3/uHnKViYis7GZrvkaGk93PgS50/5q31rsa/8lFqSgbkfMQg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.81-12.tgz",
"integrity": "sha512-XN6rK+7nv6K7Hv/fIJ4Wma+FCZFff9KaH5wES2VhWSS4Xfd4QCZQuiIWGHSmdo1MKarqp0ngx9t2VAtDY7lr7Q==",
"cpu": [
"arm64"
],
@@ -1266,9 +1266,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-x64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.81-0.tgz",
"integrity": "sha512-iZm/TkBU7WJforS+eo7mZxv4RtdoSqJBqsFwZXi8lTGSeXs2bQBnv+m+XEKabOGk9cHQ8bj8B5K/cCwro4p5tg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.81-12.tgz",
"integrity": "sha512-UUyK6Ru8kejqtTMyiiFNTNVFEPk4Qx7kaIznBLRSrR1jgrmelGjTMT6bDUlNTzqWPclUjdWF5kmCmT+urvSuqg==",
"cpu": [
"x64"
],
@@ -1285,12 +1285,12 @@
}
},
"node_modules/@github/copilot-sdk": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.11.tgz",
"integrity": "sha512-ngrnfa9052fLTOMoY0iiQS2B6pFDYJpWNj3syCdjzdje0R5mWoij9b8exJZciLvX7BbJjKz2/lIdwo24av3e3A==",
"version": "1.0.13-preview.0",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.13-preview.0.tgz",
"integrity": "sha512-iePvW3x5k1Q9OTRS1pZwLcoloyUiCtYbJsFLr6ksKxvw1xE7qhPu8B7T0lL881eMO5pWIx2La1n1b25WE3frrg==",
"license": "MIT",
"dependencies": {
"@github/copilot": "^1.0.79",
"@github/copilot": "^1.0.81-6",
"koffi": "^3.1.0",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
@@ -1299,172 +1299,10 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.80.tgz",
"integrity": "sha512-6tf93ZF56KOiTTAjK/UhLZkl1W543IzaTQly288kockJZFswpRTnQEI00Yvacpb39DTvTYu3/ha9SeKpo/pgZQ==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"detect-libc": "^2.1.2"
},
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
"@github/copilot-darwin-arm64": "1.0.80",
"@github/copilot-darwin-x64": "1.0.80",
"@github/copilot-linux-arm64": "1.0.80",
"@github/copilot-linux-x64": "1.0.80",
"@github/copilot-linuxmusl-arm64": "1.0.80",
"@github/copilot-linuxmusl-x64": "1.0.80",
"@github/copilot-win32-arm64": "1.0.80",
"@github/copilot-win32-x64": "1.0.80"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-darwin-arm64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.80.tgz",
"integrity": "sha512-fzn4PnSx3+O/a3ip72KVsjnzORsEygK+0i21bFAnFBYS+0Wi1Pk+o/CmNsJ7aRbf1enSJrcH8UDVkyc9pMGEBg==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
],
"bin": {
"copilot-darwin-arm64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-darwin-x64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.80.tgz",
"integrity": "sha512-PKsyGk5DccNzR3bYXcYTGB9N6sHzhzGqEwq/2t1qBwqPbrC98Zo2dOT2G40/QYpJ4XdrGmTmdmfPJQ9PJknlIQ==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
],
"bin": {
"copilot-darwin-x64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-linux-arm64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.80.tgz",
"integrity": "sha512-8oXwN2luyHEjIoSk8AkATBjXDhRoQtuiUvC93GpfQKFHI+I1eoOVwIsAq5fKP8jNCF2rOrYFIcTjwmRt38kCcQ==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linux-arm64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-linux-x64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.80.tgz",
"integrity": "sha512-qv1ytVNwA3IDK7kcQow+fAikD67t42+AQ8X42bK/7oudNiv4frVZMO0yh1DYIebVRcmEhmPvbVPY/ptVUK3cbA==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linux-x64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-linuxmusl-arm64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.80.tgz",
"integrity": "sha512-Qjyi+OlVnPC4Lkuy7blDMMwMUQI/yELl7gDnqQlaN8TEbhZqZueuf3p0a+kEjXcNsw4XtNYQc0eMJqSIYy/Pjg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linuxmusl-arm64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-linuxmusl-x64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.80.tgz",
"integrity": "sha512-rBg8pugf+5FhiZxi2zkOr+rlcOVF6Xg63j1FvryfwPT4DJ2w5Na7O3lpS4sgu8QmsP5H+dAqjlXYLYsvSoVQ0g==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linuxmusl-x64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-win32-arm64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.80.tgz",
"integrity": "sha512-+f7Vkd3vt2DYOxRnS8dStvYu3DY638N/AuLuIjxZp1F9GgwCUZK69wspqIxg2L59PmRRQcH4AGTrRDR60ENIZA==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
],
"bin": {
"copilot-win32-arm64": "copilot.exe"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-win32-x64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.80.tgz",
"integrity": "sha512-PO0kPqhRTWQfsqGaj4UN3cj8ttkcJYy4wmXiArtFm+03AIFu8xTvuhQDPn2xEOsUome7m7t2XomKoavcrCcRsw==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
],
"bin": {
"copilot-win32-x64": "copilot.exe"
}
},
"node_modules/@github/copilot-win32-arm64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.81-0.tgz",
"integrity": "sha512-xMNCRb3T9uBvEYarMN5zX9V8TRCM2bGoarbl5mddnP6AHyg1U7AL7CHuh6auiloE/QAPxL68ZSs3IMrAS4XcEg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.81-12.tgz",
"integrity": "sha512-ACx+faBJ/z9EzjLaZM+zEbB6+3wmj6nTjel0Q+VPD7eDhTwujSvfYGZ5nv2TMi1JN5NAua+HLE1MGhIdS1wJXw==",
"cpu": [
"arm64"
],
@@ -1478,9 +1316,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.81-0.tgz",
"integrity": "sha512-bWus/QuH1u0WUeVsafn5800JL6IwGHjNclKriQlcZsYqJ6VjUgvXJaqafdXR9Vk7DdoWkxjM76Zko3LEltDeTg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.81-12.tgz",
"integrity": "sha512-m3D/9ww1laJLQahHUjd6e0c9ziikJuTPgSotJ0hFqXXkxacrxvAFAiWduhyeotl63IShMZk4M3jz59SHV9un9w==",
"cpu": [
"x64"
],
@@ -4954,29 +4792,29 @@
"license": "MIT"
},
"node_modules/@vscode/os-proxy-resolver": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver/-/os-proxy-resolver-0.3.0.tgz",
"integrity": "sha512-JUDgHj8DQKc0h8rwYl+o1fG3ZXOJ7KuzqR+0T6LMQ39OnljBRpok/eBv/dtnx6kQF3++TkO2UASuRVypU4tSmg==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver/-/os-proxy-resolver-0.4.0.tgz",
"integrity": "sha512-bSwx24M7okqbI7B57jfARm/ICCPB4w22lXk5YlROmzJ3j5ponIiya23OqMwFNFLtoQktWkbpxxafVL0BBpbRQw==",
"license": "MIT",
"engines": {
"node": ">=22.15.0"
},
"optionalDependencies": {
"@vscode/os-proxy-resolver-darwin-arm64": "0.3.0",
"@vscode/os-proxy-resolver-darwin-x64": "0.3.0",
"@vscode/os-proxy-resolver-linux-arm-gnueabihf": "0.3.0",
"@vscode/os-proxy-resolver-linux-arm64-gnu": "0.3.0",
"@vscode/os-proxy-resolver-linux-arm64-musl": "0.3.0",
"@vscode/os-proxy-resolver-linux-x64-gnu": "0.3.0",
"@vscode/os-proxy-resolver-linux-x64-musl": "0.3.0",
"@vscode/os-proxy-resolver-win32-arm64-msvc": "0.3.0",
"@vscode/os-proxy-resolver-win32-x64-msvc": "0.3.0"
"@vscode/os-proxy-resolver-darwin-arm64": "0.4.0",
"@vscode/os-proxy-resolver-darwin-x64": "0.4.0",
"@vscode/os-proxy-resolver-linux-arm-gnueabihf": "0.4.0",
"@vscode/os-proxy-resolver-linux-arm64-gnu": "0.4.0",
"@vscode/os-proxy-resolver-linux-arm64-musl": "0.4.0",
"@vscode/os-proxy-resolver-linux-x64-gnu": "0.4.0",
"@vscode/os-proxy-resolver-linux-x64-musl": "0.4.0",
"@vscode/os-proxy-resolver-win32-arm64-msvc": "0.4.0",
"@vscode/os-proxy-resolver-win32-x64-msvc": "0.4.0"
}
},
"node_modules/@vscode/os-proxy-resolver-darwin-arm64": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-darwin-arm64/-/os-proxy-resolver-darwin-arm64-0.3.0.tgz",
"integrity": "sha512-ef9rbWVDdouTd++dKLxqw65o2D8nkMkf1LxiIvUX/S/6DzvuJQrQhqixNckC63kH3bDdV/LONF+HvrG7gcHxYQ==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-darwin-arm64/-/os-proxy-resolver-darwin-arm64-0.4.0.tgz",
"integrity": "sha512-QNYOWxTaQ9D/rUf88LjOPYAkMXsX2yGvECW/6Ii0BO5gBr7lZZRDa7lfYqjjcmIiJoxxu/83Wi1pXlB0ZT9UDw==",
"cpu": [
"arm64"
],
@@ -4987,9 +4825,9 @@
]
},
"node_modules/@vscode/os-proxy-resolver-darwin-x64": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-darwin-x64/-/os-proxy-resolver-darwin-x64-0.3.0.tgz",
"integrity": "sha512-NrJPt2OMXr7nn0XfI0i6eikrI6R012qxWXTvshFRFiOyLkdUVNRqwOpkqWlNEQuTwyqyOxe9E6QFVd6XgaF6YA==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-darwin-x64/-/os-proxy-resolver-darwin-x64-0.4.0.tgz",
"integrity": "sha512-nV9DTpit6VoVLPOpIr9NVWfDSgql8C62PWkVLumCANF5xqLsHyKjuX/UJ53/WPmOl7Sf7Z+J4tNzQpGSCdV8fg==",
"cpu": [
"x64"
],
@@ -5000,9 +4838,9 @@
]
},
"node_modules/@vscode/os-proxy-resolver-linux-arm-gnueabihf": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-arm-gnueabihf/-/os-proxy-resolver-linux-arm-gnueabihf-0.3.0.tgz",
"integrity": "sha512-lrHXnRuTZcQUdI7p7T+usSRbcnMsLHoE+F8VTQsw1DWBQXTKA3J2SXZcCf9nxNIagc/u7cSSozhFIqULqnW8Ig==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-arm-gnueabihf/-/os-proxy-resolver-linux-arm-gnueabihf-0.4.0.tgz",
"integrity": "sha512-FOMiZzOe/3IWN/FIGeefF7/R1s3uH7dwqq+4FwBagxhlmEtXfaeJMSInZ56wBrNxXUJXAI/Fsetjm+7eOfCygQ==",
"cpu": [
"arm"
],
@@ -5016,9 +4854,9 @@
]
},
"node_modules/@vscode/os-proxy-resolver-linux-arm64-gnu": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-arm64-gnu/-/os-proxy-resolver-linux-arm64-gnu-0.3.0.tgz",
"integrity": "sha512-CmQPXcjfrfvVQ446dGxeITtsGwd58kO0j207vYBjamUjYMgi1frltKPeOIYHgklJwGVQZNwqVBUIS5d45VE1Bg==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-arm64-gnu/-/os-proxy-resolver-linux-arm64-gnu-0.4.0.tgz",
"integrity": "sha512-YG9NIXolWmD7x5aLWPGP1UQKfErNonHBT9fSvKuDL6DspGyQJFKkyqudTzHu1ykSgwb1VuWmMSckQQoFhYL6ew==",
"cpu": [
"arm64"
],
@@ -5032,9 +4870,9 @@
]
},
"node_modules/@vscode/os-proxy-resolver-linux-arm64-musl": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-arm64-musl/-/os-proxy-resolver-linux-arm64-musl-0.3.0.tgz",
"integrity": "sha512-3mo9+dkB5BjnEnmLWqYkO2X58JwqWgkxDHJoFf7qfTpOV6YfqoqFnbNqLKfjQU9BBRSYg3ixcLhlkmmfMRNXtw==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-arm64-musl/-/os-proxy-resolver-linux-arm64-musl-0.4.0.tgz",
"integrity": "sha512-DnQ/dGjJoQeWgWRqGtXoJlAW1NIbewG5BmbvM8F+w5edgiTw2zLKwVpDVfaERg/Gkn+vXegfzyhOpjPaNyfXIQ==",
"cpu": [
"arm64"
],
@@ -5048,9 +4886,9 @@
]
},
"node_modules/@vscode/os-proxy-resolver-linux-x64-gnu": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-x64-gnu/-/os-proxy-resolver-linux-x64-gnu-0.3.0.tgz",
"integrity": "sha512-ymoShcbOV85b/rrzemidN6o5LJN9h+Au/h3eJhgJwRYV5+dP+JTjSMUQ5nOMHUARYFISzOO7weP2jW5hCsNlMg==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-x64-gnu/-/os-proxy-resolver-linux-x64-gnu-0.4.0.tgz",
"integrity": "sha512-aIginNUW2UjUDqiDj15PlGz4qzlWZHsNnpcTF6dHMePOGvgjJYdy7I7jNE7Itg6tkTwEh+OY0L2dyUqHRGlw9g==",
"cpu": [
"x64"
],
@@ -5064,9 +4902,9 @@
]
},
"node_modules/@vscode/os-proxy-resolver-linux-x64-musl": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-x64-musl/-/os-proxy-resolver-linux-x64-musl-0.3.0.tgz",
"integrity": "sha512-FnFEBsLeOSgTkA9mE+ezOksWBVoFaz5hJZbpNegAAMURpEGqdwtDl62InD3LGODhRd55qpVoG5HZ7c8CJZO9cA==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-linux-x64-musl/-/os-proxy-resolver-linux-x64-musl-0.4.0.tgz",
"integrity": "sha512-3f7tzRzyUmT4N9pmdY2mJV1pntkRk61UsB41/MqzI50C+N3byIAmB3HkhYtLPibDNfH8t5ISTl/vOc3ixpk1bw==",
"cpu": [
"x64"
],
@@ -5080,9 +4918,9 @@
]
},
"node_modules/@vscode/os-proxy-resolver-win32-arm64-msvc": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-win32-arm64-msvc/-/os-proxy-resolver-win32-arm64-msvc-0.3.0.tgz",
"integrity": "sha512-+rsd9UncPci+H3+HIgkDBZwHFuVOcdtv9jEG5rKgYMzhJq9KigTfjy/KtHpK9awKyAM48Qga9xvw3dediatS0Q==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-win32-arm64-msvc/-/os-proxy-resolver-win32-arm64-msvc-0.4.0.tgz",
"integrity": "sha512-g9kDAbc/PrNdWArYvJmEwrKInZzvmizV83dc2lisHqFa9g9Iq6nFT7KJMVfVjJ6ZmhijuwCYpu91ikjNMnC5Wg==",
"cpu": [
"arm64"
],
@@ -5093,9 +4931,9 @@
]
},
"node_modules/@vscode/os-proxy-resolver-win32-x64-msvc": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-win32-x64-msvc/-/os-proxy-resolver-win32-x64-msvc-0.3.0.tgz",
"integrity": "sha512-qWQeaiPNDTJYErdq90HTrgMI5Fl814PwN3wtHvEMK2qRvT1CiRfBnkv5Mj3H4TfXMINsA5rr39pygwys0e0PWg==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@vscode/os-proxy-resolver-win32-x64-msvc/-/os-proxy-resolver-win32-x64-msvc-0.4.0.tgz",
"integrity": "sha512-35NrEYB8MKug3dui465JAV83lHi8pKeM7T4yUNLLAG7z/zFO3fpbB7FgQ+rh4AyZF9QwPvEvy8UgYqXmjA36aw==",
"cpu": [
"x64"
],
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.136.0",
"distro": "406e12a29f51487d7c905069d25636a6e641097a",
"distro": "ebabac9aa9cfb4c9351977aa0d1cab83b357ffaa",
"author": {
"name": "Microsoft Corporation"
},
@@ -101,8 +101,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.82.0",
"@devcontainers/cli": "0.88.0",
"@github/copilot": "1.0.81-0",
"@github/copilot-sdk": "1.0.11",
"@github/copilot": "1.0.81-12",
"@github/copilot-sdk": "1.0.13-preview.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/dev-tunnels-connections": "^1.3.41",
@@ -120,7 +120,7 @@
"@vscode/fs-copyfile": "2.0.0",
"@vscode/iconv-lite-umd": "0.7.1",
"@vscode/native-watchdog": "^1.4.6",
"@vscode/os-proxy-resolver": "^0.3.0",
"@vscode/os-proxy-resolver": "^0.4.0",
"@vscode/policy-watcher": "^1.4.0",
"@vscode/proxy-agent": "^0.44.0",
"@vscode/ripgrep-universal": "^1.18.0",
+41 -203
View File
@@ -8,8 +8,8 @@
"name": "vscode-reh",
"version": "0.0.0",
"dependencies": {
"@github/copilot": "1.0.81-0",
"@github/copilot-sdk": "1.0.11",
"@github/copilot": "1.0.81-12",
"@github/copilot-sdk": "1.0.13-preview.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/mxc-sdk": "0.7.0",
@@ -61,9 +61,9 @@
}
},
"node_modules/@github/copilot": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.81-0.tgz",
"integrity": "sha512-0JggnsNkKQl5O6ilPxrjxDahARG/kPPRzDQvEHnuUBisfJi6PSRc4BXNtH+cc704nifIuj8DTb16lK11sRbneA==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.81-12.tgz",
"integrity": "sha512-Gs0LB6j8C2D02oagnBaX+g8U03ulT3utomXnW9TrZWdtY4JVMRIu20cr/aMkr24xATX9jUbPbTfGJAwzP7N/gA==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"detect-libc": "^2.1.2"
@@ -72,20 +72,20 @@
"copilot": "npm-loader.js"
},
"optionalDependencies": {
"@github/copilot-darwin-arm64": "1.0.81-0",
"@github/copilot-darwin-x64": "1.0.81-0",
"@github/copilot-linux-arm64": "1.0.81-0",
"@github/copilot-linux-x64": "1.0.81-0",
"@github/copilot-linuxmusl-arm64": "1.0.81-0",
"@github/copilot-linuxmusl-x64": "1.0.81-0",
"@github/copilot-win32-arm64": "1.0.81-0",
"@github/copilot-win32-x64": "1.0.81-0"
"@github/copilot-darwin-arm64": "1.0.81-12",
"@github/copilot-darwin-x64": "1.0.81-12",
"@github/copilot-linux-arm64": "1.0.81-12",
"@github/copilot-linux-x64": "1.0.81-12",
"@github/copilot-linuxmusl-arm64": "1.0.81-12",
"@github/copilot-linuxmusl-x64": "1.0.81-12",
"@github/copilot-win32-arm64": "1.0.81-12",
"@github/copilot-win32-x64": "1.0.81-12"
}
},
"node_modules/@github/copilot-darwin-arm64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.81-0.tgz",
"integrity": "sha512-8KOUMQ0OdmNQXPhjsKpo86VL/ZUT/lN4329Kdk6cpsvkM3x4+PdFNIJ7j9+4O9rB1P3g5gJ04XtR+qAkmeTekg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.81-12.tgz",
"integrity": "sha512-oaJHeCJpvr9ClfrnhFou2AZUd7SqxGrdCkBnVM1tYduUDPzb9xqdFsMcJn2g0hfPM1yO3khjWU6WQxmuQCyJnA==",
"cpu": [
"arm64"
],
@@ -99,9 +99,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.81-0.tgz",
"integrity": "sha512-6jbuDK3zyyU7Iiwnx3Shhs6LcEm03/NCnZYEJFd4oj+g4OtyCziXlVOeIGP+fntuIltIP418Dhc6qd8LL45/Qg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.81-12.tgz",
"integrity": "sha512-lYyFpCEA/usTGJSKWx1X5U4a4DiJ9A1A7vXrE0uE+HjS/9eJE2NBR1z3fPUP0V8HL0JUqAsSdM6rPbFfiE1c+Q==",
"cpu": [
"x64"
],
@@ -115,9 +115,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.81-0.tgz",
"integrity": "sha512-UUkCH9iaVdCNHQPjEdSm2L5AZyaM0x0XbxuRuhqB8KidcOCOHp2+Qr8VZnkkXA43CuYQHHi3mSkUGNR1QsEuLg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.81-12.tgz",
"integrity": "sha512-5jV7xuoqGndoQFxeM06IuEe4oEhY03PBIYyghvFVptQWY7Cj/Q3CMogIUaIQ+ugjuV8oRgBpm1JLJmr5Cw4FHA==",
"cpu": [
"arm64"
],
@@ -134,9 +134,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.81-0.tgz",
"integrity": "sha512-bAnfkeTFQcUSuUuKFLzBI8n/wrRbel54cb0aZnSYQCejnOSuYSF4d/osCTLDzZwo7aU0zWYxC38jJ9eh4HkUfQ==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.81-12.tgz",
"integrity": "sha512-/P0SMTeHQoMlV7MX+GCvIkht2uXZkLroyFMGjum+pD8XG7AsN8aozjILd5+JFEr6uxyhqnPjSOWj/vjmzmMoDA==",
"cpu": [
"x64"
],
@@ -153,9 +153,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-arm64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.81-0.tgz",
"integrity": "sha512-4Iqz4VPC5kULTC9KbyFP1NB3kmfZc+Z5vKSGhP3/uHnKViYis7GZrvkaGk93PgS50/5q31rsa/8lFqSgbkfMQg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.81-12.tgz",
"integrity": "sha512-XN6rK+7nv6K7Hv/fIJ4Wma+FCZFff9KaH5wES2VhWSS4Xfd4QCZQuiIWGHSmdo1MKarqp0ngx9t2VAtDY7lr7Q==",
"cpu": [
"arm64"
],
@@ -172,9 +172,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-x64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.81-0.tgz",
"integrity": "sha512-iZm/TkBU7WJforS+eo7mZxv4RtdoSqJBqsFwZXi8lTGSeXs2bQBnv+m+XEKabOGk9cHQ8bj8B5K/cCwro4p5tg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.81-12.tgz",
"integrity": "sha512-UUyK6Ru8kejqtTMyiiFNTNVFEPk4Qx7kaIznBLRSrR1jgrmelGjTMT6bDUlNTzqWPclUjdWF5kmCmT+urvSuqg==",
"cpu": [
"x64"
],
@@ -191,12 +191,12 @@
}
},
"node_modules/@github/copilot-sdk": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.11.tgz",
"integrity": "sha512-ngrnfa9052fLTOMoY0iiQS2B6pFDYJpWNj3syCdjzdje0R5mWoij9b8exJZciLvX7BbJjKz2/lIdwo24av3e3A==",
"version": "1.0.13-preview.0",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.13-preview.0.tgz",
"integrity": "sha512-iePvW3x5k1Q9OTRS1pZwLcoloyUiCtYbJsFLr6ksKxvw1xE7qhPu8B7T0lL881eMO5pWIx2La1n1b25WE3frrg==",
"license": "MIT",
"dependencies": {
"@github/copilot": "^1.0.79",
"@github/copilot": "^1.0.81-6",
"koffi": "^3.1.0",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
@@ -205,172 +205,10 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.80.tgz",
"integrity": "sha512-6tf93ZF56KOiTTAjK/UhLZkl1W543IzaTQly288kockJZFswpRTnQEI00Yvacpb39DTvTYu3/ha9SeKpo/pgZQ==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"detect-libc": "^2.1.2"
},
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
"@github/copilot-darwin-arm64": "1.0.80",
"@github/copilot-darwin-x64": "1.0.80",
"@github/copilot-linux-arm64": "1.0.80",
"@github/copilot-linux-x64": "1.0.80",
"@github/copilot-linuxmusl-arm64": "1.0.80",
"@github/copilot-linuxmusl-x64": "1.0.80",
"@github/copilot-win32-arm64": "1.0.80",
"@github/copilot-win32-x64": "1.0.80"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-darwin-arm64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.80.tgz",
"integrity": "sha512-fzn4PnSx3+O/a3ip72KVsjnzORsEygK+0i21bFAnFBYS+0Wi1Pk+o/CmNsJ7aRbf1enSJrcH8UDVkyc9pMGEBg==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
],
"bin": {
"copilot-darwin-arm64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-darwin-x64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.80.tgz",
"integrity": "sha512-PKsyGk5DccNzR3bYXcYTGB9N6sHzhzGqEwq/2t1qBwqPbrC98Zo2dOT2G40/QYpJ4XdrGmTmdmfPJQ9PJknlIQ==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
],
"bin": {
"copilot-darwin-x64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-linux-arm64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.80.tgz",
"integrity": "sha512-8oXwN2luyHEjIoSk8AkATBjXDhRoQtuiUvC93GpfQKFHI+I1eoOVwIsAq5fKP8jNCF2rOrYFIcTjwmRt38kCcQ==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linux-arm64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-linux-x64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.80.tgz",
"integrity": "sha512-qv1ytVNwA3IDK7kcQow+fAikD67t42+AQ8X42bK/7oudNiv4frVZMO0yh1DYIebVRcmEhmPvbVPY/ptVUK3cbA==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linux-x64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-linuxmusl-arm64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.80.tgz",
"integrity": "sha512-Qjyi+OlVnPC4Lkuy7blDMMwMUQI/yELl7gDnqQlaN8TEbhZqZueuf3p0a+kEjXcNsw4XtNYQc0eMJqSIYy/Pjg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linuxmusl-arm64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-linuxmusl-x64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.80.tgz",
"integrity": "sha512-rBg8pugf+5FhiZxi2zkOr+rlcOVF6Xg63j1FvryfwPT4DJ2w5Na7O3lpS4sgu8QmsP5H+dAqjlXYLYsvSoVQ0g==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linuxmusl-x64": "copilot"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-win32-arm64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.80.tgz",
"integrity": "sha512-+f7Vkd3vt2DYOxRnS8dStvYu3DY638N/AuLuIjxZp1F9GgwCUZK69wspqIxg2L59PmRRQcH4AGTrRDR60ENIZA==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
],
"bin": {
"copilot-win32-arm64": "copilot.exe"
}
},
"node_modules/@github/copilot-sdk/node_modules/@github/copilot-win32-x64": {
"version": "1.0.80",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.80.tgz",
"integrity": "sha512-PO0kPqhRTWQfsqGaj4UN3cj8ttkcJYy4wmXiArtFm+03AIFu8xTvuhQDPn2xEOsUome7m7t2XomKoavcrCcRsw==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
],
"bin": {
"copilot-win32-x64": "copilot.exe"
}
},
"node_modules/@github/copilot-win32-arm64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.81-0.tgz",
"integrity": "sha512-xMNCRb3T9uBvEYarMN5zX9V8TRCM2bGoarbl5mddnP6AHyg1U7AL7CHuh6auiloE/QAPxL68ZSs3IMrAS4XcEg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.81-12.tgz",
"integrity": "sha512-ACx+faBJ/z9EzjLaZM+zEbB6+3wmj6nTjel0Q+VPD7eDhTwujSvfYGZ5nv2TMi1JN5NAua+HLE1MGhIdS1wJXw==",
"cpu": [
"arm64"
],
@@ -384,9 +222,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
"version": "1.0.81-0",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.81-0.tgz",
"integrity": "sha512-bWus/QuH1u0WUeVsafn5800JL6IwGHjNclKriQlcZsYqJ6VjUgvXJaqafdXR9Vk7DdoWkxjM76Zko3LEltDeTg==",
"version": "1.0.81-12",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.81-12.tgz",
"integrity": "sha512-m3D/9ww1laJLQahHUjd6e0c9ziikJuTPgSotJ0hFqXXkxacrxvAFAiWduhyeotl63IShMZk4M3jz59SHV9un9w==",
"cpu": [
"x64"
],
+2 -2
View File
@@ -3,8 +3,8 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@github/copilot": "1.0.81-0",
"@github/copilot-sdk": "1.0.11",
"@github/copilot": "1.0.81-12",
"@github/copilot-sdk": "1.0.13-preview.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/mxc-sdk": "0.7.0",
+58 -13
View File
@@ -12,16 +12,18 @@ npm run mock-policy-server
Open `http://127.0.0.1:3000`. Managed settings is mocked by default. Use the
switch beside each endpoint tab to choose mock or passthrough. Presets apply
immediately; status and JSON edits auto-save.
immediately; response behavior, status, and JSON edits auto-save.
The GUI opens on the **Policies** workspace. Select **Setup** in the header to
open a modal that guides you through either connection method:
- **System proxy (recommended):** works with Code OSS, Stable, Insiders, Copilot
CLI, and SDK/runtime clients. The page recommends Proxyman on macOS and
provides a **Map Remote** rule. VS Code normally uses the system proxy; the
`http.proxy` setting is available as an optional fallback when explicit client
configuration is needed.
CLI, and SDK/runtime clients. The page recommends Proxyman on macOS and Windows
and provides a **Map Remote** rule, along with the per-platform toggle that
routes system traffic through Proxyman (**Tools > macOS Proxy** on macOS,
**Tools > Override Windows Proxy** on Windows). VS Code clients must also add
the displayed `http.proxy` property to `settings.json`; the copy action copies
only the property, without surrounding object braces.
- **Code OSS overrides:** the quicker option for Code OSS from this checkout.
Select **Apply Overrides**, reload, and sign in. This option does not redirect
SDK/runtime requests.
@@ -33,12 +35,26 @@ Account Policy**. To refresh the policy used by Local Agent Host, also run
The Setup dialog checks Code OSS overrides directly. It tests the system proxy by
sending a request without credentials to the managed settings URL and confirming
that the response came from this local server. It does not inspect Proxyman or
macOS proxy configuration. The test runs automatically, and the global header
always shows a green or red connection indicator.
the operating system's proxy configuration. The test runs automatically, and the
global header always shows a green or red connection indicator.
If no real request appears in **Live Requests**, use **Clear Policy Cache**. A
fresh managed-settings cache entry can prevent the client from making a request
for up to one hour. Then run the commands above again.
If no real request appears in **Live Requests**, open **Clear SDK Policy Cache**,
expand the section for the client platform, and run the copied command in a
terminal. A fresh managed-settings cache entry can prevent the client from making
a request for up to one hour. Then run the commands above again.
macOS:
```sh
rm -rf -- "${COPILOT_CACHE_HOME:-$HOME/Library/Caches/copilot}/managed-settings"
```
Windows PowerShell:
```powershell
$root = if ($env:COPILOT_CACHE_HOME) { $env:COPILOT_CACHE_HOME } elseif ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA 'copilot' } else { Join-Path $HOME '.cache\copilot' }; $path = Join-Path $root 'managed-settings'; if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force }
```
Select a policy endpoint request in **Live Requests** to open its response editor.
Requests that do not match one of the four policy endpoints remain read-only.
Other Copilot clients share that cache. For an isolated run, start both the
server and Code OSS with the same temporary cache home:
@@ -91,9 +107,38 @@ curl -X POST "$BASE/api/state" \
]}'
```
A preset sets its status and body and enables mocking. Explicit `status`, `body`,
or `active` values in the same update override the preset. Invalid requests are
rejected before any endpoint changes.
A preset sets the status and body and enables mocking. Response behavior is
configured independently with `mode`, including when a preset and mode are sent
in the same update. Explicit `status`, `body`, or `active` values override the
preset. Invalid requests are rejected before any endpoint changes. Supported
response modes are `json`, `malformed-json`, `disconnect`, and `timeout`.
### Test fail-closed managed-settings refresh
First serve a successful policy that enables the forced-refresh requirement and
sync it into VS Code. Then configure an HTTP error preset or a failing response
behavior and sync again. Seeding the requirement first mirrors a real deployment
where the cached control self-perpetuates through an outage.
```sh
curl -X POST "$BASE/api/state" \
-H 'Content-Type: application/json' \
-d '{"endpoint":"managedSettings","preset":"customization-lockdown"}'
# Run "Developer: Sync Account Policy" in VS Code, then choose one:
curl -X POST "$BASE/api/state" -H 'Content-Type: application/json' \
-d '{"endpoint":"managedSettings","preset":"server-error"}'
curl -X POST "$BASE/api/state" -H 'Content-Type: application/json' \
-d '{"endpoint":"managedSettings","mode":"malformed-json","status":200}'
curl -X POST "$BASE/api/state" -H 'Content-Type: application/json' \
-d '{"endpoint":"managedSettings","mode":"disconnect"}'
curl -X POST "$BASE/api/state" -H 'Content-Type: application/json' \
-d '{"endpoint":"managedSettings","mode":"timeout"}'
```
These configurations exercise HTTP error, malformed response, immediate network
failure, and client-timeout paths respectively. Clear the policy cache if the
request does not appear in **Live Requests**.
| Method | Route | Purpose |
| --- | --- | --- |
+91 -1
View File
@@ -32,6 +32,8 @@ export interface EndpointPreset {
body: unknown;
}
export type EndpointResponseMode = 'json' | 'malformed-json' | 'disconnect' | 'timeout';
export interface EndpointDef {
/** Stable id used by the API + GUI. */
id: string;
@@ -86,7 +88,7 @@ declare var MOCK_POLICY_ENDPOINTS: EndpointDef[];
{
id: 'disable-bypass-permissions',
label: 'Disable bypass permissions',
description: 'Disables bypass permissions mode.',
description: 'Blocks all escalation to bypass-permissions ("allow-all"/"yolo") mode, including auto-approval.',
status: 200,
body: {
permissions: {
@@ -94,6 +96,87 @@ declare var MOCK_POLICY_ENDPOINTS: EndpointDef[];
}
}
},
{
id: 'allow-auto-only',
label: 'Allow auto-approval only',
description: 'Blocks full allow-all bypass but still permits advisory auto-approval (LLM safety recommendations with normal prompt paths).',
status: 200,
body: {
permissions: {
disableBypassPermissionsMode: 'allow-auto-only'
}
}
},
{
id: 'deny-dangerous-commands',
label: 'Deny dangerous shell/file operations',
description: 'Blocks specific shell commands, workspace-scoped file writes, and a domain outright. A single leading slash means the workspace root in the managed permission syntax.',
status: 200,
body: {
permissions: {
deny: [
'Shell(rm -rf *)',
'Shell(curl *)',
'Write(/.github/workflows/**)',
'Domain(evil.example.com)'
]
}
}
},
{
id: 'workspace-scoped-paths',
label: 'Workspace-scoped paths',
description: 'Demonstrates paths relative to the workspace root: /src/** and /test/** match only inside the workspace, while /package.json targets that workspace file.',
status: 200,
body: {
permissions: {
ask: [
'Write(/src/**)',
'Write(/test/**)'
],
deny: [
'Write(/package.json)'
]
}
}
},
{
id: 'ask-before-publish',
label: 'Ask before publishing or deploying',
description: 'Requires human approval for package publish/deploy commands and writes anywhere under the user home directory, including workspaces located there. It does not cover paths outside the home directory.',
status: 200,
body: {
permissions: {
ask: [
'Shell(npm publish *)',
'Shell(git push *)',
'Write(~/**)'
]
}
}
},
{
id: 'lockdown-allowlist',
label: 'Lockdown: allow only an approved set',
description: 'Intersects with any other managed allow list, so only requests every managed source admits run without prompting. Combine with deny/ask for defense in depth.',
status: 200,
body: {
permissions: {
disableBypassPermissionsMode: 'disable',
allow: [
'Read(**)',
'Shell(git status)',
'Shell(git diff *)',
'Domain(github.com)',
'Domain(*.githubusercontent.com)'
],
deny: [
'Write(/.github/workflows/**)',
'Write(~/.ssh/**)'
]
}
}
},
{
id: 'model-auto',
label: 'Model: auto',
@@ -158,6 +241,13 @@ declare var MOCK_POLICY_ENDPOINTS: EndpointDef[];
client_version: '1.132.0',
minimum_client_version: '1.133.0'
}
},
{
id: 'server-error',
label: 'Server error (500)',
description: 'Returns an HTTP 500 response to exercise the fail-closed HTTP error path.',
status: 500,
body: { error: 'mock_managed_settings_failure' }
}
]
},
+100 -25
View File
@@ -12,6 +12,7 @@
* `endpoints.ts` (loaded via an earlier `<script>` tag).
*/
type EndpointDef = import('../endpoints').EndpointDef;
type EndpointResponseMode = import('../endpoints').EndpointResponseMode;
declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
@@ -22,6 +23,7 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
url?: string;
status?: number;
body?: unknown;
mode?: EndpointResponseMode;
active?: boolean;
}
@@ -41,10 +43,6 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
status: number;
}
interface CacheResult {
cleared: { dir: string; files: number }[];
}
interface SchemaResult {
ok: boolean;
resolved?: string;
@@ -80,6 +78,7 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
endpoint: string;
status: number;
body: unknown;
mode: EndpointResponseMode;
active: boolean;
editorText: string;
}
@@ -90,13 +89,17 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
const tabs = $('tabs');
const editor = $('editor') as HTMLTextAreaElement;
const responseStatusInput = $('response-status') as HTMLInputElement;
const responseModeSelect = $('response-mode') as HTMLSelectElement;
const responseConfiguration = $('response-configuration');
const responseStatusValidation = $('response-status-validation');
const presetSelect = $('preset') as HTMLSelectElement;
const endpointMeta = $('endpoint-meta');
const editorStatus = $('editor-status');
const saveStateEl = $('save-state');
const setupDialog = $('setup-dialog') as HTMLDialogElement;
const vscodeProxySettings = '{\n\t"http.proxy": "http://localhost:9090"\n}';
const vscodeProxySetting = '"http.proxy": "http://localhost:9090"';
const macOsCacheClearCommand = 'rm -rf -- "${COPILOT_CACHE_HOME:-$HOME/Library/Caches/copilot}/managed-settings"';
const windowsCacheClearCommand = '$root = if ($env:COPILOT_CACHE_HOME) { $env:COPILOT_CACHE_HOME } elseif ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA \'copilot\' } else { Join-Path $HOME \'.cache\\copilot\' }; $path = Join-Path $root \'managed-settings\'; if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force }';
let endpoints: Endpoint[] = [];
let activeId = '';
@@ -107,6 +110,7 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
let proxyBaseUrl = '';
let proxyUpstream = '';
let proxyCheckInFlight = false;
let renderedLogSignature = '';
let stateUpdateQueue: Promise<void> = Promise.resolve();
const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>();
@@ -402,6 +406,8 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
endpointMeta.replaceChildren(routeSpan);
responseStatusInput.value = String(endpoint.status ?? 200);
responseModeSelect.value = endpoint.mode ?? 'json';
updateResponseConfigurationVisibility();
parseResponseStatus();
editor.value = drafts[id] ?? JSON.stringify(endpoint.body ?? {}, null, '\t');
renderTabs();
@@ -455,6 +461,34 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
updateReadiness();
}
function updateResponseConfigurationVisibility(): void {
responseConfiguration.hidden = responseModeSelect.value !== 'json';
}
async function setResponseMode(endpoint: Endpoint, mode: EndpointResponseMode): Promise<void> {
const previousMode = endpoint.mode ?? 'json';
endpoint.mode = mode;
updateResponseConfigurationVisibility();
const snapshot = captureSaveSnapshot();
clearPendingSave(endpoint.id);
if (snapshot) {
if (!await saveSnapshot(snapshot)) {
endpoint.mode = previousMode;
responseModeSelect.value = previousMode;
updateResponseConfigurationVisibility();
}
return;
}
try {
applyState(await updateState({ endpoint: endpoint.id, mode }));
} catch (error) {
endpoint.mode = previousMode;
responseModeSelect.value = previousMode;
updateResponseConfigurationVisibility();
toast(`Save failed: ${error instanceof Error ? error.message : String(error)}`, true);
}
}
function renderProxy(): void {
const endpoint = endpoints.find(candidate => candidate.id === 'managedSettings') ?? endpoints[0];
$('map-from').textContent = endpoint && proxyUpstream ? `${proxyUpstream}${endpoint.path}` : '';
@@ -620,6 +654,7 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
endpoint: endpoint.id,
status: responseStatus,
body: parsed,
mode: endpoint.mode ?? 'json',
active: endpoint.active === true,
editorText: editor.value
};
@@ -631,6 +666,7 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
endpoint: snapshot.endpoint,
status: snapshot.status,
body: snapshot.body,
mode: snapshot.mode,
active: snapshot.active
});
applyState(state);
@@ -824,13 +860,41 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
}
const list = $('log');
$('log-empty').hidden = entries.length > 0;
const displayedEntries = entries.slice(0, 40);
const signature = JSON.stringify(displayedEntries);
if (signature === renderedLogSignature) {
return;
}
renderedLogSignature = signature;
const focusedEntryKey = document.activeElement instanceof HTMLButtonElement && document.activeElement.classList.contains('log-item')
? document.activeElement.dataset.entryKey
: undefined;
let rowToRefocus: HTMLButtonElement | undefined;
list.replaceChildren();
for (const entry of entries.slice(0, 40)) {
for (const entry of displayedEntries) {
const item = document.createElement('li');
item.className = `log-item ${entry.outcome}`;
item.title = entry.outcome === 'mocked'
item.className = 'log-row';
const endpoint = endpoints.find(candidate => candidate.path === entry.path);
const entryKey = `${entry.at}:${entry.method}:${entry.path}`;
const outcomeLabel = entry.outcome === 'mocked'
? 'Served from this server'
: entry.outcome === 'passthrough' ? 'Proxied to the real API' : 'Upstream request failed';
let row: HTMLButtonElement | HTMLDivElement;
if (endpoint) {
row = document.createElement('button');
row.type = 'button';
row.classList.add('clickable');
row.setAttribute('aria-label', `Open ${endpoint.label}: ${entry.method} ${entry.path}, status ${entry.status}. ${outcomeLabel}`);
row.addEventListener('click', () => selectEndpoint(endpoint.id));
if (focusedEntryKey === entryKey) {
rowToRefocus = row;
}
} else {
row = document.createElement('div');
}
row.classList.add('log-item', entry.outcome);
row.dataset.entryKey = entryKey;
row.title = endpoint ? `Open ${endpoint.label}. ${outcomeLabel}` : outcomeLabel;
const time = document.createElement('span');
time.className = 'log-time';
@@ -842,21 +906,17 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
route.className = 'log-path';
route.textContent = `${entry.method} ${entry.path}`;
item.append(time, status, route);
row.append(time, status, route);
if (!endpoint) {
const outcome = document.createElement('span');
outcome.className = 'sr-only';
outcome.textContent = `. ${outcomeLabel}`;
row.appendChild(outcome);
}
item.appendChild(row);
list.appendChild(item);
}
}
async function clearPolicyCache(): Promise<void> {
try {
const result = await api<CacheResult>('/api/cache', { method: 'DELETE' });
const count = result.cleared.reduce((total, item) => total + item.files, 0);
toast(result.cleared.length === 0
? 'No managed-settings cache found \u2014 nothing to clear'
: `Cleared ${count} cached ${count === 1 ? 'entry' : 'entries'} \u2014 restart Local Agent Host to refetch`);
} catch (e) {
toast(`Could not clear cache: ${e instanceof Error ? e.message : String(e)}`, true);
}
rowToRefocus?.focus();
}
async function copy(text: string, button: HTMLElement): Promise<void> {
@@ -864,6 +924,7 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
await navigator.clipboard.writeText(text);
const original = button.textContent;
button.textContent = 'Copied \u2713';
toast('Copied to clipboard');
setTimeout(() => { button.textContent = original; }, 1500);
} catch {
toast('Copy failed — check clipboard permissions', true);
@@ -881,7 +942,9 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
}
async function init(): Promise<void> {
$('proxy-settings').textContent = vscodeProxySettings;
$('proxy-settings').textContent = vscodeProxySetting;
$('macos-cache-command').textContent = macOsCacheClearCommand;
$('windows-cache-command').textContent = windowsCacheClearCommand;
editor.addEventListener('input', () => {
drafts[activeId] = editor.value;
@@ -894,13 +957,26 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
parseEditor();
debouncedSave();
});
responseModeSelect.addEventListener('change', () => {
const endpoint = activeEndpoint();
if (!endpoint) {
return;
}
void setResponseMode(endpoint, responseModeSelect.value as EndpointResponseMode);
});
presetSelect.addEventListener('change', applyPreset);
$('overrides-action').addEventListener('click', () => wire(!overridesWired));
$('copy-map').addEventListener('click', e => {
copy(`${$('map-from').textContent}\n${$('map-to').textContent}`, e.currentTarget as HTMLElement);
});
$('copy-proxy-settings').addEventListener('click', e => {
copy(vscodeProxySettings, e.currentTarget as HTMLElement);
copy(vscodeProxySetting, e.currentTarget as HTMLElement);
});
$('copy-macos-cache-command').addEventListener('click', e => {
copy(macOsCacheClearCommand, e.currentTarget as HTMLElement);
});
$('copy-windows-cache-command').addEventListener('click', e => {
copy(windowsCacheClearCommand, e.currentTarget as HTMLElement);
});
for (const method of ['proxy', 'overrides'] as const) {
$(`setup-method-${method}`).addEventListener('change', () => selectSetupMethod(method));
@@ -937,7 +1013,6 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
void save();
setStatus('Generated an example from the schema.', 'ok');
});
$('clear-cache').addEventListener('click', () => { void clearPolicyCache(); });
$('clear-log').addEventListener('click', async () => {
try {
await api('/api/log', { method: 'DELETE' });
@@ -964,7 +1039,7 @@ declare const MOCK_POLICY_ENDPOINTS: EndpointDef[];
selectSetupMethod('proxy');
// Fall back to the shared endpoint definitions so the GUI still shows
// what exists (read-only) rather than rendering a blank page.
endpoints = MOCK_POLICY_ENDPOINTS.map(def => ({ ...def, status: def.presets[0]?.status ?? 200, body: def.presets[0]?.body ?? {} }));
endpoints = MOCK_POLICY_ENDPOINTS.map(def => ({ ...def, status: def.presets[0]?.status ?? 200, body: def.presets[0]?.body ?? {}, mode: 'json' }));
renderTabs();
if (endpoints.length) {
selectEndpoint(endpoints[0].id);
+86 -46
View File
@@ -31,7 +31,22 @@
</div>
<a id="policies-nav" href="#policies" aria-current="page">Policies</a>
</nav>
<button id="clear-cache" type="button" class="btn-header">Clear Policy Cache</button>
<details class="cache-command-picker">
<summary class="btn-header">Clear SDK Policy Cache</summary>
<div class="cache-command-popover">
<p class="helper-text">Copy the command for the client platform, then run it in a terminal.</p>
<details class="cache-platform">
<summary>macOS</summary>
<code id="macos-cache-command" class="block cache-command"></code>
<button id="copy-macos-cache-command" type="button" class="btn-secondary">Copy macOS Command</button>
</details>
<details class="cache-platform">
<summary>Windows (PowerShell)</summary>
<code id="windows-cache-command" class="block cache-command"></code>
<button id="copy-windows-cache-command" type="button" class="btn-secondary">Copy PowerShell Command</button>
</details>
</div>
</details>
</div>
</div>
</header>
@@ -60,8 +75,8 @@
<label class="method-label" for="setup-method-proxy">
<span class="method-badge">Recommended</span>
<h3>System Proxy</h3>
<span class="method-recommendation">Recommended on macOS: Proxyman</span>
<p>Routes policy requests from Code OSS, Stable, Insiders, Copilot CLI, and SDK/runtime clients.</p>
<span class="method-recommendation">Recommended on macOS and Windows: Proxyman</span>
<p>Routes policy requests from Code OSS, Stable, Insiders, Copilot CLI, and SDK/runtime clients. VS Code clients also require the proxy setting shown below.</p>
</label>
</div>
<div class="method-status">
@@ -91,20 +106,33 @@
<details>
<summary>Proxyman tips</summary>
<ul>
<li>Turn on macOS Proxy in Proxyman before checking the connection.</li>
<li>If HTTPS requests do not appear, install and trust Proxyman's certificate.</li>
<li>Keep the source and destination paths identical.</li>
<li>If HTTPS requests do not appear, install and trust Proxyman's certificate.</li>
</ul>
<div class="platform-tips">
<section>
<h5>macOS</h5>
<ul>
<li>Turn on <code>Tools &gt; macOS Proxy</code> before checking the connection.</li>
</ul>
</section>
<section>
<h5>Windows</h5>
<ul>
<li>Turn on <code>Tools &gt; Override Windows Proxy</code> before checking the connection. Proxyman still shows its listening port while this is off, but Windows leaves system proxying disabled, so clients connect directly and no requests are captured.</li>
<li>Confirm Windows picked up the change: <code>ProxyEnable</code> should be <code>1</code> and <code>ProxyServer</code> should be Proxyman's host and port under <code>HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings</code>.</li>
</ul>
</section>
</div>
</details>
<details>
<summary>Optional: configure VS Code explicitly</summary>
<p>VS Code normally uses the system proxy. If it does not in your environment, add this to the client's <code>settings.json</code>.</p>
<div class="required-setting">
<p><strong>Required for VS Code:</strong> add this property inside the client's <code>settings.json</code>.</p>
<div class="code-field">
<span class="label-small">VS Code settings.json</span>
<span class="label-small">VS Code settings.json property</span>
<code id="proxy-settings" class="block settings-json"></code>
</div>
<button id="copy-proxy-settings" type="button" class="btn-secondary">Copy VS Code Setting</button>
</details>
<button id="copy-proxy-settings" type="button" class="btn-secondary">Copy Setting Property</button>
</div>
</div>
</li>
<li>
@@ -113,7 +141,7 @@
<h4>Request fresh policy in VS Code</h4>
<p>Open the Command Palette and run <code>&gt; Developer: Sync Account Policy</code>.</p>
<p>To refresh the policy used by Local Agent Host, also run <code>&gt; Developer: Restart Local Agent Host</code>.</p>
<p class="helper-text">If no request appears under Live Requests on the Policies page, clear the policy cache from the header and run the commands again.</p>
<p class="helper-text">If no request appears under Live Requests on the Policies page, open Clear SDK Policy Cache in the header, copy the command for your platform, run it in a terminal, and then run the commands again.</p>
</div>
</li>
</ol>
@@ -146,7 +174,7 @@
<div class="step-content">
<h4>Reload and request policy</h4>
<p>Reload Code OSS, sign in, open the Command Palette, and run <code>&gt; Developer: Sync Account Policy</code>.</p>
<p class="helper-text">If no request appears under Live Requests on the Policies page, clear the policy cache from the header and run the command again.</p>
<p class="helper-text">If no request appears under Live Requests on the Policies page, open Clear SDK Policy Cache in the header, copy the command for your platform, run it in a terminal, and then run the command again.</p>
</div>
</li>
</ol>
@@ -169,42 +197,54 @@
<p id="endpoint-meta" class="endpoint-meta"></p>
<div class="form-group">
<label for="preset">Preset</label>
<select id="preset"></select>
<label for="response-mode">Response behavior</label>
<select id="response-mode">
<option value="json">JSON response</option>
<option value="malformed-json">Malformed JSON</option>
<option value="disconnect">Disconnect without response</option>
<option value="timeout">No response (client timeout)</option>
</select>
</div>
<div class="form-group">
<label for="response-status">HTTP status code</label>
<input id="response-status" class="status-code-input" type="number" min="200" max="599" step="1"
inputmode="numeric" aria-describedby="response-status-validation">
<p id="response-status-validation" class="status" aria-live="polite"></p>
</div>
<div id="schema-section" class="section-block" hidden>
<h2 class="schema-heading">
<button type="button" class="section-header section-header-toggle" id="schema-toggle"
aria-expanded="false" aria-controls="schema-details">
<span class="chevron" id="schema-chevron" aria-hidden="true">&#9654;</span>
<span>Schema</span>
<span id="schema-badge" class="schema-badge" data-tooltip="">&hellip;</span>
</button>
</h2>
<div id="schema-details" class="schema-details" hidden>
<span class="label-small">Schema source</span>
<code id="schema-source" class="block"></code>
<button id="hydrate-schema" type="button" class="btn-secondary">Generate Example</button>
<div id="response-configuration" class="response-configuration">
<div class="form-group">
<label for="preset">Preset</label>
<select id="preset"></select>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label for="editor">Response body (JSON)</label>
<span id="save-state" class="save-state" aria-live="polite"></span>
<div class="form-group">
<label for="response-status">HTTP status code</label>
<input id="response-status" class="status-code-input" type="number" min="200" max="599" step="1"
inputmode="numeric" aria-describedby="response-status-validation">
<p id="response-status-validation" class="status" aria-live="polite"></p>
</div>
<div id="schema-section" class="section-block" hidden>
<h2 class="schema-heading">
<button type="button" class="section-header section-header-toggle" id="schema-toggle"
aria-expanded="false" aria-controls="schema-details">
<span class="chevron" id="schema-chevron" aria-hidden="true">&#9654;</span>
<span>Schema</span>
<span id="schema-badge" class="schema-badge" data-tooltip="">&hellip;</span>
</button>
</h2>
<div id="schema-details" class="schema-details" hidden>
<span class="label-small">Schema source</span>
<code id="schema-source" class="block"></code>
<button id="hydrate-schema" type="button" class="btn-secondary">Generate Example</button>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label for="editor">Response body (JSON)</label>
<span id="save-state" class="save-state" aria-live="polite"></span>
</div>
<textarea id="editor" spellcheck="false" autocomplete="off"
aria-describedby="editor-status"></textarea>
<p id="editor-status" class="status" aria-live="polite"></p>
<div id="validation-results" hidden></div>
</div>
<textarea id="editor" spellcheck="false" autocomplete="off"
aria-describedby="editor-status"></textarea>
<p id="editor-status" class="status" aria-live="polite"></p>
<div id="validation-results" hidden></div>
</div>
</div>
</section>
@@ -212,11 +252,11 @@
<aside class="sidebar-panel">
<div class="section-block">
<div class="section-header">
<h2>Live Requests</h2>
<h2 id="live-requests-title">Live Requests</h2>
<button id="clear-log" type="button" class="btn-link">Clear</button>
</div>
<p id="log-empty" class="helper-text">No requests yet.</p>
<ul id="log" class="log"></ul>
<ul id="log" class="log" aria-labelledby="live-requests-title"></ul>
</div>
</aside>
</div>
+127 -1
View File
@@ -202,6 +202,68 @@ h2 {
color: var(--text);
}
.cache-command-picker {
position: relative;
}
.cache-command-picker > summary {
display: block;
list-style: none;
cursor: pointer;
border: 1px solid transparent;
border-radius: 4px;
font-family: inherit;
font-size: inherit;
transition: all 0.2s ease;
white-space: nowrap;
user-select: none;
}
.cache-command-picker > summary::-webkit-details-marker {
display: none;
}
.cache-command-picker[open] > summary {
border-color: var(--border);
background: var(--surface-alt);
color: var(--text);
}
.cache-command-popover {
position: absolute;
inset-block-start: calc(100% + 8px);
inset-inline-end: 0;
z-index: 10;
display: flex;
width: min(560px, calc(100vw - 48px));
flex-direction: column;
gap: 8px;
padding: 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
.cache-platform {
border-top: 1px solid var(--border);
padding-top: 8px;
}
.cache-platform > summary {
width: fit-content;
cursor: pointer;
font-weight: 600;
}
.cache-platform .cache-command {
margin: 8px 0;
max-height: 160px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.app-main {
max-width: 1400px;
margin: 0 auto;
@@ -256,6 +318,17 @@ h2 {
min-width: 0;
}
.response-configuration {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0;
}
.response-configuration[hidden] {
display: none;
}
.section-block {
background: var(--surface);
border: 1px solid var(--border);
@@ -593,6 +666,25 @@ h2 {
min-height: 56px;
}
.required-setting {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
.required-setting > p {
margin: 0 0 10px;
color: var(--text-secondary);
}
.required-setting .code-field {
margin-bottom: 10px;
}
.required-setting .code-field code.block {
min-height: 0;
}
.step-content details {
margin-top: 12px;
color: var(--text-secondary);
@@ -618,6 +710,24 @@ h2 {
margin-bottom: 10px;
}
.platform-tips {
display: grid;
gap: 12px;
margin-top: 12px;
}
.platform-tips h5 {
margin: 0;
color: var(--text);
font-size: 12px;
font-weight: 600;
}
.platform-tips ul {
margin: 6px 0 0;
padding-left: 20px;
}
.setup-steps.compact .step-content > p {
margin-bottom: 10px;
}
@@ -1276,19 +1386,35 @@ code {
font-size: 11px;
}
.log-row {
margin: 0;
padding: 0;
}
.log-item {
display: flex;
width: 100%;
gap: 8px;
align-items: baseline;
padding: 3px 6px;
border: 0;
border-left: 2px solid transparent;
border-radius: 2px;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
transition: background 0.2s ease;
}
.log-item:nth-child(odd) {
.log-row:nth-child(odd) .log-item {
background: color-mix(in srgb, var(--text) 4%, transparent);
}
.log-item.clickable:hover {
background: var(--accent-soft);
}
.log-item.mocked {
border-left-color: var(--ok);
}
+49 -8
View File
@@ -6,7 +6,7 @@
/** Local mock and passthrough proxy for the Copilot policy endpoints. */
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { EndpointDef } from './endpoints';
import type { EndpointDef, EndpointResponseMode } from './endpoints';
const http = require('node:http') as typeof import('node:http');
const https = require('node:https') as typeof import('node:https');
@@ -56,6 +56,7 @@ const GUI_ASSETS = new Map<string, string>([
interface EndpointState {
status: number;
body: unknown;
mode: EndpointResponseMode;
/** When false the endpoint is proxied upstream instead of mocked. */
active: boolean;
}
@@ -68,6 +69,7 @@ interface EndpointUpdate {
preset?: string;
status?: number;
body?: unknown;
mode?: EndpointResponseMode;
active?: boolean;
}
@@ -135,8 +137,7 @@ const server = http.createServer((req, res) => {
return;
}
if (req.method === 'GET') {
record(req, pathname, 'mocked', entry.status);
return sendJson(res, entry.status, entry.body);
return sendMockedResponse(req, res, pathname, entry);
}
}
@@ -166,7 +167,7 @@ function handleControlApi(req: IncomingMessage, res: ServerResponse, pathname: s
return sendJson(res, 200, {
name: 'Mock Policy Server Control API',
stateUpdate: {
single: { endpoint: 'managedSettings', preset: 'empty', status: 200, body: {}, active: true },
single: { endpoint: 'managedSettings', preset: 'empty', status: 200, body: {}, mode: 'json', active: true },
bulk: { endpoints: [{ endpoint: 'managedSettings', active: true }, { endpoint: 'entitlements', active: false }] }
},
routes: [
@@ -278,7 +279,7 @@ function applyEndpointUpdates(payload: unknown): { ok: true } | { ok: false; err
return { ok: false, error: `${prefix} must be a JSON object.` };
}
const unknownKeys = Object.keys(rawUpdate).filter(key => !['endpoint', 'preset', 'status', 'body', 'active'].includes(key));
const unknownKeys = Object.keys(rawUpdate).filter(key => !['endpoint', 'preset', 'status', 'body', 'mode', 'active'].includes(key));
if (unknownKeys.length) {
return { ok: false, error: `${prefix} has unknown field${unknownKeys.length > 1 ? 's' : ''}: ${unknownKeys.join(', ')}.` };
}
@@ -294,8 +295,8 @@ function applyEndpointUpdates(payload: unknown): { ok: true } | { ok: false; err
if (!def) {
return { ok: false, error: `Unknown endpoint "${rawUpdate.endpoint}". Valid endpoints: ${endpoints.map(endpoint => endpoint.id).join(', ')}.` };
}
if (!['preset', 'status', 'body', 'active'].some(key => Object.hasOwn(rawUpdate, key))) {
return { ok: false, error: `${prefix} must include preset, status, body, or active.` };
if (!['preset', 'status', 'body', 'mode', 'active'].some(key => Object.hasOwn(rawUpdate, key))) {
return { ok: false, error: `${prefix} must include preset, status, body, mode, or active.` };
}
if (Object.hasOwn(rawUpdate, 'preset')) {
if (typeof rawUpdate.preset !== 'string') {
@@ -313,6 +314,9 @@ function applyEndpointUpdates(payload: unknown): { ok: true } | { ok: false; err
if (Object.hasOwn(rawUpdate, 'active') && typeof rawUpdate.active !== 'boolean') {
return { ok: false, error: `${prefix}.active must be a boolean.` };
}
if (Object.hasOwn(rawUpdate, 'mode') && !isEndpointResponseMode(rawUpdate.mode)) {
return { ok: false, error: `${prefix}.mode must be one of: json, malformed-json, disconnect, timeout.` };
}
const update: EndpointUpdate = { endpoint: rawUpdate.endpoint };
if (typeof rawUpdate.preset === 'string') {
@@ -324,6 +328,9 @@ function applyEndpointUpdates(payload: unknown): { ok: true } | { ok: false; err
if (Object.hasOwn(rawUpdate, 'body')) {
update.body = rawUpdate.body;
}
if (isEndpointResponseMode(rawUpdate.mode)) {
update.mode = rawUpdate.mode;
}
if (typeof rawUpdate.active === 'boolean') {
update.active = rawUpdate.active;
}
@@ -332,7 +339,7 @@ function applyEndpointUpdates(payload: unknown): { ok: true } | { ok: false; err
const nextState = new Map<string, EndpointState>();
for (const [id, entry] of state) {
nextState.set(id, { status: entry.status, body: clone(entry.body), active: entry.active });
nextState.set(id, { status: entry.status, body: clone(entry.body), mode: entry.mode, active: entry.active });
}
for (const update of updates) {
@@ -350,6 +357,9 @@ function applyEndpointUpdates(payload: unknown): { ok: true } | { ok: false; err
if (Object.hasOwn(update, 'body')) {
entry.body = clone(update.body);
}
if (update.mode !== undefined) {
entry.mode = update.mode;
}
if (update.active !== undefined) {
entry.active = update.active;
}
@@ -368,6 +378,7 @@ function resetEndpointState(): void {
state.set(endpoint.id, {
status: preset?.status ?? 200,
body: preset ? clone(preset.body) : {},
mode: 'json',
active: endpoint.mockedByDefault === true
});
}
@@ -653,6 +664,7 @@ function getState() {
presets: e.presets,
status: state.get(e.id)!.status,
body: state.get(e.id)!.body,
mode: state.get(e.id)!.mode,
active: state.get(e.id)!.active
})),
wired: isWired(),
@@ -828,6 +840,35 @@ function sendJson(res: ServerResponse, status: number, obj: unknown): void {
res.end(JSON.stringify(obj, null, 2));
}
function sendMockedResponse(req: IncomingMessage, res: ServerResponse, pathname: string, entry: EndpointState): void {
switch (entry.mode) {
case 'json':
record(req, pathname, 'mocked', entry.status);
sendJson(res, entry.status, entry.body);
return;
case 'malformed-json':
record(req, pathname, 'mocked', entry.status);
res.writeHead(entry.status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
res.end('{"unterminated":');
return;
case 'disconnect':
record(req, pathname, 'mocked', 0);
res.destroy();
return;
case 'timeout': {
record(req, pathname, 'mocked', 0);
const timer = setTimeout(() => res.destroy(), 10_000);
timer.unref();
res.on('close', () => clearTimeout(timer));
return;
}
}
}
function isEndpointResponseMode(value: unknown): value is EndpointResponseMode {
return value === 'json' || value === 'malformed-json' || value === 'disconnect' || value === 'timeout';
}
function readBody(req: IncomingMessage, cb: (err: Error | null, raw: string) => void): void {
let raw = '';
req.on('data', chunk => { raw += chunk; if (raw.length > 1_000_000) { req.destroy(); } });
+66 -27
View File
@@ -5,7 +5,7 @@
import { IContextMenuProvider } from '../../contextmenu.js';
import * as DOM from '../../dom.js';
import { ActionBar, ActionsOrientation, IActionViewItemProvider } from '../actionbar/actionbar.js';
import { ActionBar, ActionsOrientation, IActionViewItem, IActionViewItemProvider } from '../actionbar/actionbar.js';
import { AnchorAlignment, IContextViewCloseAnimation } from '../contextview/contextview.js';
import { DropdownMenuActionViewItem } from '../dropdown/dropdownActionViewItem.js';
import { Action, IAction, IActionRunner, Separator, SubmenuAction } from '../../../common/actions.js';
@@ -30,6 +30,8 @@ export interface IToolBarResponsiveBehaviorOptions {
readonly minItems?: number;
readonly actionMinWidth?: number;
readonly getActionMinWidth?: (action: IAction) => number | undefined;
readonly allowOverflow?: boolean | (() => boolean);
readonly getOverflowAction?: (action: IAction, getAnchor: () => HTMLElement | undefined) => IAction;
readonly observedElement?: HTMLElement;
readonly getAvailableWidth?: () => number;
}
@@ -73,6 +75,8 @@ export interface IToolBarOptions {
* - `minItems`: The minimum number of items that should always be visible.
* - `actionMinWidth`: The minimum width of each action item. Defaults to `ACTION_MIN_WIDTH` (24px).
* - `getActionMinWidth`: Optional per-action minimum width override in pixels.
* - `allowOverflow`: Whether actions may move into the overflow menu, or a callback that decides from current presentation state.
* - `getOverflowAction`: Replaces an action only while it is rendered in the overflow menu.
*/
responsiveBehavior?: IToolBarResponsiveBehaviorOptions;
}
@@ -234,6 +238,15 @@ export class ToolBar extends Disposable {
return this.actionBar.getWidth(index);
}
getItemElement(index: number): HTMLElement | undefined {
const element = this.actionBar.getContainer().firstElementChild?.children.item(index);
return DOM.isHTMLElement(element) ? element : undefined;
}
getItemViewItem(index: number): IActionViewItem | undefined {
return this.actionBar.viewItems[index];
}
private getUnshrunkItemWidth(index: number): number {
const actionItem = this.actionBar.getContainer().firstElementChild?.children.item(index);
if (!DOM.isHTMLElement(actionItem)) {
@@ -258,6 +271,10 @@ export class ToolBar extends Disposable {
return this.actionBar.length();
}
hasOverflow(): boolean {
return this.actionBar.hasAction(this.toggleMenuAction);
}
setAriaLabel(label: string): void {
this.actionBar.setAriaLabel(label);
}
@@ -416,22 +433,33 @@ export class ToolBar extends Disposable {
// Each action is assumed to have a minimum width so that actions with a label
// can shrink to the action's minimum width. We do this so that action visibility
// takes precedence over the action label.
const isActionItemVisible = (index: number): boolean => {
const element = this.getItemElement(index);
return !element || DOM.getWindow(element).getComputedStyle(element).display !== 'none';
};
const getVisiblePrimaryActionIndexes = (): number[] => {
const indexes: number[] = [];
for (let index = 0; index < this.actionBar.length(); index++) {
if (this.actionBar.getAction(index) !== this.toggleMenuAction && isActionItemVisible(index)) {
indexes.push(index);
}
}
return indexes;
};
const actionBarMinimumWidth = () => {
if (this.options.responsiveBehavior?.kind === 'last') {
const hasToggleMenuAction = this.actionBar.hasAction(this.toggleMenuAction);
const primaryActionsCount = hasToggleMenuAction
? this.actionBar.length() - 1
: this.actionBar.length();
if (primaryActionsCount === 0) {
const primaryActionIndexes = getVisiblePrimaryActionIndexes();
if (primaryActionIndexes.length === 0) {
return hasToggleMenuAction ? ACTION_MIN_WIDTH + ACTION_PADDING : 0;
}
let itemsWidth = 0;
for (let i = 0; i < primaryActionsCount - 1; i++) {
itemsWidth += this.actionBar.getWidth(i) + ACTION_PADDING;
for (const index of primaryActionIndexes.slice(0, -1)) {
itemsWidth += this.actionBar.getWidth(index) + ACTION_PADDING;
}
const action = this.actionBar.getAction(primaryActionsCount - 1);
const action = this.actionBar.getAction(primaryActionIndexes.at(-1)!);
itemsWidth += this.getActionMinWidth(action); // item to shrink
itemsWidth += hasToggleMenuAction ? ACTION_MIN_WIDTH + ACTION_PADDING : 0; // toggle menu action
@@ -439,7 +467,9 @@ export class ToolBar extends Disposable {
} else {
let itemsWidth = 0;
for (let i = 0; i < this.actionBar.length(); i++) {
itemsWidth += this.getActionMinWidth(this.actionBar.getAction(i));
if (isActionItemVisible(i)) {
itemsWidth += this.getActionMinWidth(this.actionBar.getAction(i));
}
}
return itemsWidth;
}
@@ -448,20 +478,17 @@ export class ToolBar extends Disposable {
const projectedActionBarMinimumWidth = (actionToAdd: IAction, keepToggleMenuAction: boolean) => {
let itemsWidth = this.getActionMinWidth(actionToAdd);
if (this.options.responsiveBehavior?.kind === 'last') {
const hasToggleMenuAction = this.actionBar.hasAction(this.toggleMenuAction);
const primaryActionsCount = hasToggleMenuAction
? this.actionBar.length() - 1
: this.actionBar.length();
for (let i = 0; i < primaryActionsCount; i++) {
const itemWidth = i === primaryActionsCount - 1
? this.getUnshrunkItemWidth(i)
: this.actionBar.getWidth(i);
const primaryActionIndexes = getVisiblePrimaryActionIndexes();
for (const [position, index] of primaryActionIndexes.entries()) {
const itemWidth = position === primaryActionIndexes.length - 1
? this.getUnshrunkItemWidth(index)
: this.actionBar.getWidth(index);
itemsWidth += itemWidth + ACTION_PADDING;
}
} else {
for (let i = 0; i < this.actionBar.length(); i++) {
const action = this.actionBar.getAction(i);
if (action && action !== this.toggleMenuAction) {
if (action && action !== this.toggleMenuAction && isActionItemVisible(i)) {
itemsWidth += this.getActionMinWidth(action);
}
}
@@ -480,11 +507,14 @@ export class ToolBar extends Disposable {
}
if (minimumWidth > containerWidth) {
const allowOverflow = this.options.responsiveBehavior?.allowOverflow;
if (allowOverflow === false || (typeof allowOverflow === 'function' && !allowOverflow())) {
return;
}
// Check for max items limit
if (this.options.responsiveBehavior?.minItems !== undefined) {
const primaryActionsCount = this.actionBar.hasAction(this.toggleMenuAction)
? this.actionBar.length() - 1
: this.actionBar.length();
const primaryActionsCount = getVisiblePrimaryActionIndexes().length;
if (primaryActionsCount <= this.options.responsiveBehavior.minItems) {
return;
@@ -493,12 +523,16 @@ export class ToolBar extends Disposable {
// Hide actions from the right
while (minimumWidth > containerWidth && this.actionBar.length() > 0) {
const index = this.originalPrimaryActions.length - this.hiddenActions.length - 1;
if (index < 0) {
const index = getVisiblePrimaryActionIndexes().at(-1);
if (index === undefined) {
break;
}
const action = this.originalPrimaryActions[index];
this.hiddenActions.unshift(action);
const action = this.actionBar.getAction(index);
if (!action) {
break;
}
this.hiddenActions.push(action);
this.hiddenActions.sort((a, b) => this.originalPrimaryActions.indexOf(a) - this.originalPrimaryActions.indexOf(b));
// Remove the action
this.actionBar.pull(index);
@@ -534,7 +568,9 @@ export class ToolBar extends Disposable {
icon: this.options.icon ?? true,
label: this.options.label ?? false,
keybinding: this.getKeybindingLabel(action),
index: this.originalPrimaryActions.length - this.hiddenActions.length - 1
index: this.originalPrimaryActions
.slice(0, this.originalPrimaryActions.indexOf(action))
.reduce((index, precedingAction) => index + (this.actionBar.hasAction(precedingAction) ? 1 : 0), 0)
});
// There are no secondary actions, and there is only one hidden item left so we
@@ -550,7 +586,10 @@ export class ToolBar extends Disposable {
}
// Update overflow menu
const hiddenActions = this.hiddenActions.slice(0);
const hiddenActions = this.hiddenActions.map(action => this.options.responsiveBehavior?.getOverflowAction?.(
action,
() => this.toggleMenuActionViewItem?.element,
) ?? action);
if (this.originalSecondaryActions.length > 0 || hiddenActions.length > 0) {
const secondaryActions = this.originalSecondaryActions.slice(0);
this.toggleMenuAction.menuActions = Separator.join(hiddenActions, secondaryActions);
@@ -13,12 +13,13 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.j
class FixedWidthActionViewItem extends BaseActionViewItem {
constructor(action: IAction, private readonly width: number) {
constructor(action: IAction, private readonly width: number, private readonly visible = true) {
super(undefined, action);
}
override render(container: HTMLElement): void {
super.render(container);
container.style.display = this.visible ? '' : 'none';
container.style.width = `${this.width}px`;
container.style.boxSizing = 'border-box';
container.style.overflow = 'hidden';
@@ -110,7 +111,7 @@ suite('ToolBar', () => {
assert.strictEqual(toolbar.getItemAction(1)?.id, 'workbench.action.chat.openModePicker');
assert.strictEqual(toolbar.getItemAction(2)?.id, 'workbench.action.chat.openModelPicker');
assert.strictEqual(toolbar.getItemAction(3)?.id, ToggleMenuAction.ID);
assert.strictEqual(toolbar.getElement().querySelector('.monaco-action-bar')?.classList.contains('has-overflow'), true);
assert.strictEqual(toolbar.hasOverflow(), true);
});
test('applies per-action responsive min widths', () => {
@@ -404,13 +405,155 @@ suite('ToolBar', () => {
// availableWidth = 200 is plenty for all 3 actions; the element's 0 width is ignored
assert.strictEqual(toolbar.getItemsLength(), 3);
assert.strictEqual(toolbar.getElement().querySelector('.monaco-action-bar')?.classList.contains('has-overflow'), false);
assert.strictEqual(toolbar.hasOverflow(), false);
availableWidth = 60;
toolbar.relayout();
// availableWidth shrank — actions overflow into the toggle menu
assert.strictEqual(toolbar.getItemAction(toolbar.getItemsLength() - 1)?.id, ToggleMenuAction.ID);
assert.strictEqual(toolbar.getElement().querySelector('.monaco-action-bar')?.classList.contains('has-overflow'), true);
assert.strictEqual(toolbar.hasOverflow(), true);
availableWidth = 200;
toolbar.relayout();
assert.strictEqual(toolbar.getItemsLength(), 3);
assert.strictEqual(toolbar.hasOverflow(), false);
});
test('ignores non-rendered actions when deciding to overflow', () => {
const hiddenActionIds = new Set(['hidden.a', 'hidden.b', 'hidden.c']);
const toolbar = store.add(new TestToolBar(container, contextMenuProvider, {
responsiveBehavior: {
enabled: true,
kind: 'all',
minItems: 1,
actionMinWidth: 48,
getActionMinWidth: () => 22,
getAvailableWidth: () => 60,
},
actionViewItemProvider: action => new FixedWidthActionViewItem(action, 22, !hiddenActionIds.has(action.id)),
}));
toolbar.setActions([
store.add(new Action('hidden.a', 'Hidden A')),
store.add(new Action('hidden.b', 'Hidden B')),
store.add(new Action('visible.a', 'Visible A')),
store.add(new Action('hidden.c', 'Hidden C')),
store.add(new Action('visible.b', 'Visible B')),
]);
assert.deepStrictEqual({
visibleActions: Array.from({ length: toolbar.getItemsLength() }, (_, index) => ({
id: toolbar.getItemAction(index)?.id,
display: toolbar.getItemElement(index)?.style.display,
})).filter(item => item.display !== 'none').map(item => item.id),
overflow: toolbar.hasOverflow(),
}, {
visibleActions: ['visible.a', 'visible.b'],
overflow: false,
});
});
test('can keep compact actions visible instead of overflowing', () => {
const toolbar = store.add(new TestToolBar(container, contextMenuProvider, {
responsiveBehavior: {
enabled: true,
kind: 'all',
minItems: 1,
actionMinWidth: 22,
getAvailableWidth: () => 50,
allowOverflow: false,
},
actionViewItemProvider: action => new FixedWidthActionViewItem(action, 22),
}));
toolbar.setActions([
store.add(new Action('a', 'A')),
store.add(new Action('b', 'B')),
store.add(new Action('c', 'C')),
]);
assert.deepStrictEqual({
items: Array.from({ length: toolbar.getItemsLength() }, (_, index) => toolbar.getItemAction(index)?.id),
overflow: toolbar.hasOverflow(),
}, {
items: ['a', 'b', 'c'],
overflow: false,
});
});
test('allows overflow only after compact actions still exceed the width', () => {
let availableWidth = 100;
let allCompact = false;
const toolbar = store.add(new TestToolBar(container, contextMenuProvider, {
responsiveBehavior: {
enabled: true,
kind: 'all',
minItems: 1,
actionMinWidth: 22,
getAvailableWidth: () => availableWidth,
allowOverflow: () => allCompact,
},
actionViewItemProvider: action => new FixedWidthActionViewItem(action, 22),
}));
toolbar.setActions([
store.add(new Action('a', 'A')),
store.add(new Action('b', 'B')),
store.add(new Action('c', 'C')),
]);
availableWidth = 50;
toolbar.relayout();
const beforeCompact = toolbar.hasOverflow();
allCompact = true;
toolbar.relayout();
const afterCompact = toolbar.hasOverflow();
assert.deepStrictEqual({ beforeCompact, afterCompact }, {
beforeCompact: false,
afterCompact: true,
});
});
test('uses overflow-specific proxy actions', async () => {
const runs: string[] = [];
let overflowAnchor: HTMLElement | undefined;
const toolbar = store.add(new TestToolBar(container, contextMenuProvider, {
responsiveBehavior: {
enabled: true,
kind: 'all',
minItems: 1,
actionMinWidth: 22,
getAvailableWidth: () => 50,
getOverflowAction: (action, getAnchor) => ({
...action,
run: () => {
overflowAnchor = getAnchor();
runs.push(`overflow:${action.id}`);
},
}),
},
actionViewItemProvider: action => new FixedWidthActionViewItem(action, 22),
}));
toolbar.setActions([
store.add(new Action('a', 'A', undefined, true, () => runs.push('original:a'))),
store.add(new Action('b', 'B', undefined, true, () => runs.push('original:b'))),
store.add(new Action('c', 'C', undefined, true, () => runs.push('original:c'))),
]);
const overflowAction = toolbar.getItemAction(toolbar.getItemsLength() - 1);
assert.strictEqual(overflowAction?.id, ToggleMenuAction.ID);
await (overflowAction as ToggleMenuAction).menuActions[0].run();
const overflowViewItem = toolbar.getItemViewItem(toolbar.getItemsLength() - 1);
const overflowButton = overflowViewItem instanceof BaseActionViewItem ? overflowViewItem.element : undefined;
assert.deepStrictEqual({
runs,
usesOverflowButton: overflowAnchor === overflowButton,
}, {
runs: ['overflow:b'],
usesOverflowButton: true,
});
});
});
@@ -2,7 +2,7 @@
<!DOCTYPE html>
<html>
<head>
<script>
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
performance.mark('code/didStartRenderer');
</script>
<meta charset="utf-8" />
@@ -18,12 +18,14 @@
<!-- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">
<meta id="vscode-workbench-web-base-url" data-settings="{{WORKBENCH_WEB_BASE_URL}}">
<!-- Workbench Auth Session -->
<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">
<!-- Builtin Extensions (running out of sources) -->
<meta id="vscode-workbench-builtin-extensions" data-settings="{{WORKBENCH_BUILTIN_EXTENSIONS}}">
<meta id="vscode-workbench-css-modules" data-settings="{{WORKBENCH_DEV_CSS_MODULES}}">
<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="{{WORKBENCH_WEB_BASE_URL}}/resources/server/favicon.ico" type="image/x-icon" />
@@ -36,18 +38,18 @@
</body>
<!-- Startup (do not modify order of script tags!) -->
<script>
const baseUrl = new URL('{{WORKBENCH_WEB_BASE_URL}}', window.location.origin).toString();
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
const baseUrl = new URL(document.getElementById('vscode-workbench-web-base-url').getAttribute('data-settings'), window.location.origin).toString();
globalThis._VSCODE_FILE_ROOT = baseUrl + '/out/';
</script>
<script>
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
const sheet = document.getElementById('vscode-css-modules').sheet;
globalThis._VSCODE_CSS_LOAD = function (url) {
sheet.insertRule(`@import url(${url});`);
};
const importMap = { imports: {} };
const cssModules = JSON.parse('{{WORKBENCH_DEV_CSS_MODULES}}');
const cssModules = JSON.parse(document.getElementById('vscode-workbench-css-modules').getAttribute('data-settings'));
for (const cssModule of cssModules) {
const cssUrl = new URL(cssModule, globalThis._VSCODE_FILE_ROOT).href;
const jsSrc = `globalThis._VSCODE_CSS_LOAD('${cssUrl}');\n`;
@@ -56,11 +58,11 @@
}
const importMapElement = document.createElement('script');
importMapElement.type = 'importmap';
importMapElement.setAttribute('nonce', '1nline-m4p')
importMapElement.nonce = document.currentScript.nonce;
importMapElement.textContent = JSON.stringify(importMap, undefined, 2);
document.head.appendChild(importMapElement);
</script>
<script>
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
performance.mark('code/willLoadWorkbenchMain');
</script>
<script type="module" src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/code/browser/workbench/workbench.js"></script>
+5 -4
View File
@@ -2,7 +2,7 @@
<!DOCTYPE html>
<html>
<head>
<script>
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
performance.mark('code/didStartRenderer');
</script>
<meta charset="utf-8" />
@@ -18,6 +18,7 @@
<!-- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">
<meta id="vscode-workbench-web-base-url" data-settings="{{WORKBENCH_WEB_BASE_URL}}">
<!-- Workbench Auth Session -->
<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">
@@ -33,11 +34,11 @@
</body>
<!-- Startup (do not modify order of script tags!) -->
<script>
const baseUrl = new URL('{{WORKBENCH_WEB_BASE_URL}}', window.location.origin).toString();
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
const baseUrl = new URL(document.getElementById('vscode-workbench-web-base-url').getAttribute('data-settings'), window.location.origin).toString();
globalThis._VSCODE_FILE_ROOT = baseUrl + '/out/';
</script>
<script>
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
performance.mark('code/willLoadWorkbenchMain');
</script>
<!-- always ensure built in english NLS messages -->
+18
View File
@@ -429,6 +429,20 @@ export class CodeApplication extends Disposable {
}
}
if (uri.scheme === Schemas.vscodeManagedRemoteResource) {
let frame: WebFrameMain | null | undefined = details.frame;
if (!frame || frame.isDestroyed()) {
this.logService.error('Blocked vscode-managed-remote-resource request', details.url);
return callback({ cancel: true });
}
for (; frame; frame = frame.parent) {
if (frame.isDestroyed() || frame.url.startsWith(`${Schemas.vscodeWebview}://`)) {
this.logService.error('Blocked vscode-managed-remote-resource request', details.url);
return callback({ cancel: true });
}
}
}
// Block most svgs
if (uri.path.endsWith('.svg')) {
const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme);
@@ -832,6 +846,10 @@ export class CodeApplication extends Disposable {
return callback(notFound());
}
if (!request.referrer || request.referrer.startsWith(`${Schemas.vscodeWebview}://`)) {
return callback(notFound());
}
remoteResourceChannel.value.call<NodeRemoteResourceResponse>(NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, [url]).then(
r => callback({ ...r, data: Buffer.from(r.body, 'base64') }),
err => {
@@ -7,6 +7,7 @@ import * as dom from '../../../base/browser/dom.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { Position } from '../../common/core/position.js';
import { TextDirection } from '../../common/model.js';
import { ViewContext } from '../../common/viewModel/viewContext.js';
import { NavigationCommandRevealType } from '../coreCommands.js';
import { IMouseTarget, IMouseTargetOutsideEditor } from '../editorBrowser.js';
@@ -197,20 +198,18 @@ export class LeftRightDragScrollingOperation extends DragScrollingOperation {
}
const edgeLineNumber = this._position.position.lineNumber;
// First, try to find a position that matches the horizontal position of the mouse
let mouseTarget: IMouseTarget;
{
const editorPos = createEditorPagePosition(this._viewHelper.viewDomNode);
const horizontalScrollbarHeight = this._context.configuration.options.get(EditorOption.layoutInfo).horizontalScrollbarHeight;
const pos = new PageCoordinates(this._mouseEvent.pos.x, editorPos.y + editorPos.height - horizontalScrollbarHeight - 0.1);
const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos);
mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null);
}
if (this._position.outsidePosition === 'left') {
mouseTarget = MouseTarget.createOutsideEditor(mouseTarget.mouseColumn, new Position(edgeLineNumber, mouseTarget.mouseColumn), 'left', this._position.outsideDistance);
// In case of RTL, the line is exceeded on the left. Otherwise on the right.
const isRtl = this._context.viewModel.getTextDirection(edgeLineNumber) === TextDirection.RTL;
const exceedingPosition = isRtl ? 'left' : 'right';
if (this._position.outsidePosition === exceedingPosition) {
// Move the selection to the far end of the line.
const lineWidth = this._context.viewModel.getLineMaxColumn(edgeLineNumber);
mouseTarget = MouseTarget.createOutsideEditor(lineWidth, new Position(edgeLineNumber, lineWidth), 'right', this._position.outsideDistance);
} else {
mouseTarget = MouseTarget.createOutsideEditor(mouseTarget.mouseColumn, new Position(edgeLineNumber, mouseTarget.mouseColumn), 'right', this._position.outsideDistance);
// Move the selection to the beginning of the line.
mouseTarget = MouseTarget.createOutsideEditor(1, new Position(edgeLineNumber, 1), 'left', this._position.outsideDistance);
}
this._dispatchMouse(mouseTarget, true, NavigationCommandRevealType.None);
@@ -0,0 +1,129 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { OffsetRange } from '../../../common/core/ranges/offsetRange.js';
export type CompressedVirtualizedItemVisibility = 'before' | 'visible' | 'after';
export interface ICompressedVirtualizedItemLayout {
/** Complete item range in outer scroll coordinates. */
readonly contentRange: OffsetRange;
/** Item range in the compressed rendered coordinate system. */
readonly renderedRange: OffsetRange;
/** Height removed from the rendered row and represented by item-local scrolling. */
readonly maxScrollOffset: number;
/**
* Item-local scroll coordinates:
*
* 0 ---------------------------- maxScrollOffset
* item begins item end is aligned
*/
readonly scrollOffset: number;
readonly visibility: CompressedVirtualizedItemVisibility;
}
export interface ICompressedVirtualizedScrollLayout {
/** Clamped position in the complete outer scroll coordinates. */
readonly scrollTop: number;
/** Complete height of all items and inter-item gaps. */
readonly scrollHeight: number;
/** Height occupied by the compressed item rows and inter-item gaps. */
readonly renderedHeight: number;
readonly contentViewport: OffsetRange;
readonly renderedViewport: OffsetRange;
/** Full-content height above the viewport represented by item-local scrolling instead of DOM movement. */
readonly hiddenContentHeightAboveViewport: number;
readonly items: readonly ICompressedVirtualizedItemLayout[];
}
export interface ICompressedVirtualizedScrollLayoutInput {
readonly scrollTop: number;
readonly viewportHeight: number;
readonly itemGap: number;
readonly itemHeights: readonly number[];
}
/**
* Computes the complete and compressed vertical layout of a virtualized list.
*/
export function computeCompressedVirtualizedScrollLayout(input: ICompressedVirtualizedScrollLayoutInput): ICompressedVirtualizedScrollLayout {
assertNonNegative('viewportHeight', input.viewportHeight);
const scrollHeight = computeCompressedVirtualizedScrollHeight(input.itemHeights, input.itemGap);
const maxScrollTop = Math.max(0, scrollHeight - input.viewportHeight);
const scrollTop = Math.max(0, Math.min(input.scrollTop, maxScrollTop));
const contentViewport = OffsetRange.ofStartAndLength(scrollTop, input.viewportHeight);
let contentTop = 0;
let renderedTop = 0;
let hiddenContentHeightAboveViewport = 0;
const items: ICompressedVirtualizedItemLayout[] = [];
for (let index = 0; index < input.itemHeights.length; index++) {
const fullHeight = input.itemHeights[index];
const renderedHeight = Math.min(fullHeight, input.viewportHeight);
const maxScrollOffset = fullHeight - renderedHeight;
const contentRange = OffsetRange.ofStartAndLength(contentTop, fullHeight);
const renderedRange = OffsetRange.ofStartAndLength(renderedTop, renderedHeight);
let visibility: CompressedVirtualizedItemVisibility;
let scrollOffset: number;
if (contentRange.isBefore(contentViewport)) {
visibility = 'before';
scrollOffset = maxScrollOffset;
} else if (contentRange.isAfter(contentViewport)) {
visibility = 'after';
scrollOffset = 0;
} else {
visibility = 'visible';
scrollOffset = Math.max(0, Math.min(contentViewport.start - contentRange.start, maxScrollOffset));
}
hiddenContentHeightAboveViewport += scrollOffset;
items.push({
contentRange,
renderedRange,
maxScrollOffset,
scrollOffset,
visibility,
});
if (index < input.itemHeights.length - 1) {
contentTop += fullHeight + input.itemGap;
renderedTop += renderedHeight + input.itemGap;
} else {
contentTop += fullHeight;
renderedTop += renderedHeight;
}
}
const renderedScrollTop = scrollTop - hiddenContentHeightAboveViewport;
return {
scrollTop,
scrollHeight,
renderedHeight: renderedTop,
contentViewport,
renderedViewport: OffsetRange.ofStartAndLength(renderedScrollTop, input.viewportHeight),
hiddenContentHeightAboveViewport,
items,
};
}
export function computeCompressedVirtualizedScrollHeight(itemHeights: readonly number[], itemGap: number): number {
assertNonNegative('itemGap', itemGap);
for (let i = 0; i < itemHeights.length; i++) {
assertNonNegative(`itemHeights[${i}]`, itemHeights[i]);
}
if (itemHeights.length === 0) {
return 0;
}
return itemHeights.reduce((result, height) => result + height, 0) + itemGap * (itemHeights.length - 1);
}
function assertNonNegative(name: string, value: number): void {
if (!Number.isFinite(value) || value < 0) {
throw new BugIndicatingError(`${name} must be a finite non-negative number, got ${value}`);
}
}
@@ -0,0 +1,213 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension, getWindow, h, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js';
import { SmoothScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { compareBy, numberComparator } from '../../../../base/common/arrays.js';
import { findFirstMax } from '../../../../base/common/arraysFind.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { autorun, derived, globalTransaction, IObservable, IReader, observableFromEvent } from '../../../../base/common/observable.js';
import { INewScrollPosition, IScrollPosition, Scrollable, ScrollbarVisibility } from '../../../../base/common/scrollable.js';
import { OffsetRange } from '../../../common/core/ranges/offsetRange.js';
import { ObservableElementSizeObserver } from '../diffEditor/utils.js';
import { computeCompressedVirtualizedScrollLayout, ICompressedVirtualizedScrollLayout } from './compressedVirtualizedScrollLayout.js';
export interface ICompressedVirtualizedScrollItemVerticalState {
/** Complete item height in outer scroll coordinates. */
readonly contentHeight: number;
/**
* Item-local scroll coordinates reported by the rendered item:
*
* 0 ---------------------------- maxScrollOffset
* item begins item end is aligned
*/
readonly itemViewportOffset: number;
}
export interface ICompressedVirtualizedScrollItem {
readonly verticalState: IObservable<ICompressedVirtualizedScrollItemVerticalState>;
readonly maxScroll: IObservable<{ readonly maxScroll: number }>;
render(renderedRange: OffsetRange, scrollOffset: number, width: number, renderedViewport: OffsetRange): void;
hide(): void;
}
export interface ICompressedVirtualizedScrollViewContext {
readonly contentDomNode: HTMLElement;
readonly overflowWidgetsDomNode: HTMLElement;
readonly scrollLeft: IObservable<number>;
}
/**
* Virtualizes complete-height items into viewport-capped rows whose removed height is represented by item-local scrolling.
*/
export class CompressedVirtualizedScrollView<TItem extends ICompressedVirtualizedScrollItem> extends Disposable {
private readonly _scrollableElements;
private readonly _scrollable;
private readonly _scrollableElement;
private readonly _sizeObserver;
private readonly _items;
private readonly _itemViewportPositions = new Map<TItem, number>();
private _anchorItem: TItem | undefined;
readonly domNode: HTMLElement;
readonly scrollTop: IObservable<number>;
readonly scrollLeft: IObservable<number>;
readonly layout: IObservable<ICompressedVirtualizedScrollLayout>;
readonly scrollDimensions: IObservable<{ readonly width: number; readonly height: number; readonly scrollWidth: number; readonly scrollHeight: number }>;
constructor(
elementToObserve: HTMLElement,
dimension: IObservable<Dimension | undefined>,
itemGap: IObservable<number>,
createItems: (context: ICompressedVirtualizedScrollViewContext) => IObservable<readonly TItem[]>,
) {
super();
this._scrollableElements = h('div.scrollContent', [
h('div@content', {
style: {
overflow: 'hidden',
position: 'relative',
}
}),
h('div.monaco-editor@overflowWidgetsDomNode'),
]);
this._scrollable = this._register(new Scrollable({
forceIntegerValues: false,
scheduleAtNextAnimationFrame: callback => scheduleAtNextAnimationFrame(getWindow(elementToObserve), callback),
smoothScrollDuration: 100,
}));
this._scrollableElement = this._register(new SmoothScrollableElement(this._scrollableElements.root, {
vertical: ScrollbarVisibility.Auto,
horizontal: ScrollbarVisibility.Auto,
useShadows: false,
}, this._scrollable));
this.domNode = h('div', {}, [this._scrollableElement.getDomNode()]).root;
this._sizeObserver = this._register(new ObservableElementSizeObserver(elementToObserve, undefined));
this.scrollTop = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop);
this.scrollLeft = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft);
this._items = createItems({
contentDomNode: this._scrollableElements.content,
overflowWidgetsDomNode: this._scrollableElements.overflowWidgetsDomNode,
scrollLeft: this.scrollLeft,
});
this.layout = derived(this, reader => computeCompressedVirtualizedScrollLayout({
scrollTop: this.scrollTop.read(reader),
viewportHeight: this._sizeObserver.height.read(reader),
itemGap: itemGap.read(reader),
itemHeights: this._items.read(reader).map(item => item.verticalState.read(reader).contentHeight),
}));
this.scrollDimensions = derived(this, reader => {
const width = this._sizeObserver.width.read(reader);
const items = this._items.read(reader);
const max = findFirstMax(items, compareBy(item => item.maxScroll.read(reader).maxScroll, numberComparator));
const maxScroll = max?.maxScroll.read(reader).maxScroll ?? 0;
return {
width,
height: this._sizeObserver.height.read(reader),
scrollWidth: width + maxScroll,
scrollHeight: this.layout.read(reader).scrollHeight,
};
});
this._register(autorun(reader => {
this._sizeObserver.observe(dimension.read(reader));
}));
this._register(autorun(reader => {
const dimensions = this.scrollDimensions.read(reader);
this._scrollableElements.root.style.height = `${dimensions.height}px`;
this._scrollableElements.content.style.height = `${dimensions.scrollHeight}px`;
this._scrollableElement.setScrollDimensions(dimensions);
}));
this._register(autorun(reader => {
globalTransaction(() => this._render(reader));
}));
}
setScrollPosition(position: INewScrollPosition, smooth = false): void {
this._scrollableElement.setScrollPosition({
...position,
reuseAnimation: smooth,
});
}
getScrollPosition(): IScrollPosition {
return this._scrollableElement.getScrollPosition();
}
private _deltaScrollVertical(delta: number): boolean {
const scrollTop = this.getScrollPosition().scrollTop;
this.setScrollPosition({ scrollTop: scrollTop + delta });
return this.getScrollPosition().scrollTop !== scrollTop;
}
private _render(reader: IReader): void {
const layout = this.layout.read(reader);
const width = this._sizeObserver.width.read(reader);
const items = this._items.read(reader);
const verticalStates = items.map(item => item.verticalState.read(reader));
const currentItems = new Set(items);
for (const item of this._itemViewportPositions.keys()) {
if (!currentItems.has(item)) {
this._itemViewportPositions.delete(item);
}
}
let anchorIndex = this._anchorItem ? items.indexOf(this._anchorItem) : -1;
let itemViewportDelta = anchorIndex >= 0 ? this._getItemViewportDelta(items[anchorIndex], layout, verticalStates, anchorIndex) : 0;
if (itemViewportDelta === 0) {
anchorIndex = layout.items.findIndex((itemLayout, index) =>
itemLayout.visibility === 'visible'
&& items[index] !== this._anchorItem
&& this._getItemViewportDelta(items[index], layout, verticalStates, index) !== 0
);
if (anchorIndex >= 0) {
itemViewportDelta = this._getItemViewportDelta(items[anchorIndex], layout, verticalStates, anchorIndex);
}
}
for (let index = 0; index < items.length; index++) {
this._itemViewportPositions.set(items[index], layout.items[index].contentRange.start + verticalStates[index].itemViewportOffset);
}
if (itemViewportDelta !== 0) {
if (anchorIndex >= 0) {
this._anchorItem = items[anchorIndex];
}
if (this._deltaScrollVertical(itemViewportDelta)) {
return;
}
}
for (let index = 0; index < items.length; index++) {
const item = items[index];
const itemLayout = layout.items[index];
if (itemLayout.visibility !== 'visible') {
item.hide();
} else {
item.render(itemLayout.renderedRange, itemLayout.scrollOffset, width, layout.renderedViewport);
}
this._itemViewportPositions.set(item, itemLayout.contentRange.start + item.verticalState.get().itemViewportOffset);
}
const currentAnchorIndex = this._anchorItem ? items.indexOf(this._anchorItem) : -1;
if (currentAnchorIndex < 0 || layout.items[currentAnchorIndex].visibility !== 'visible') {
const firstVisibleItemIndex = layout.items.findIndex(itemLayout => itemLayout.visibility === 'visible');
this._anchorItem = firstVisibleItemIndex >= 0 ? items[firstVisibleItemIndex] : undefined;
}
this._scrollableElements.content.style.transform = `translateY(${-layout.renderedViewport.start}px)`;
}
private _getItemViewportDelta(
item: TItem,
layout: ICompressedVirtualizedScrollLayout,
verticalStates: readonly ICompressedVirtualizedScrollItemVerticalState[],
index: number,
): number {
const previousPosition = this._itemViewportPositions.get(item);
if (previousPosition === undefined) {
return 0;
}
const position = layout.items[index].contentRange.start + verticalStates[index].itemViewportOffset;
return position - previousPosition;
}
}
@@ -2,10 +2,10 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { addDisposableListener, EventHelper, EventType, h } from '../../../../base/browser/dom.js';
import { addDisposableListener, EventHelper, EventType, getWindow, h, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js';
import { Button } from '../../../../base/browser/ui/button/button.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { autorun, derived, globalTransaction, IObservable, observableValue } from '../../../../base/common/observable.js';
import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
@@ -18,6 +18,7 @@ import { IDiffEditorOptions } from '../../../common/config/editorOptions.js';
import { OffsetRange } from '../../../common/core/ranges/offsetRange.js';
import { observableCodeEditor } from '../../observableCodeEditor.js';
import { DiffEditorWidget } from '../diffEditor/diffEditorWidget.js';
import { ICompressedVirtualizedScrollItemVerticalState } from './compressedVirtualizedScrollView.js';
import { DocumentDiffItemViewModel } from './multiDiffEditorViewModel.js';
import { IObjectData, IPooledObject } from './objectPool.js';
import { ActionRunnerWithContext } from './utils.js';
@@ -26,7 +27,6 @@ import { IWorkbenchUIElementFactory, MultiDiffEditorItemLabelKind } from './work
export class TemplateData implements IObjectData {
constructor(
public readonly viewModel: DocumentDiffItemViewModel,
public readonly deltaScrollVertical: (delta: number) => void,
) { }
@@ -41,7 +41,8 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
private readonly _collapsed;
private readonly _editorContentHeight;
public readonly contentHeight;
private readonly _itemViewportOffset;
public readonly verticalState: IObservable<ICompressedVirtualizedScrollItemVerticalState>;
private readonly _modifiedContentWidth;
private readonly _modifiedWidth;
@@ -61,6 +62,9 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
private readonly _resourceLabel;
private readonly _resourceLabel2;
private readonly _verticalStateUpdate = this._register(new MutableDisposable());
private _observedEditorContentHeight = 500;
private _observedItemViewportOffset = 0;
private readonly _outerEditorHeight: number;
private readonly _contextKeyService: IScopedContextKeyService;
@@ -77,9 +81,13 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
this._viewModel = observableValue<DocumentDiffItemViewModel | undefined>(this, undefined);
this._collapsed = derived(this, reader => this._viewModel.read(reader)?.collapsed.read(reader));
this._editorContentHeight = observableValue<number>(this, 500);
this.contentHeight = derived(this, reader => {
const h = this._collapsed.read(reader) ? 0 : this._editorContentHeight.read(reader);
return h + this._outerEditorHeight;
this._itemViewportOffset = observableValue<number>(this, 0);
this.verticalState = derived(this, reader => {
const collapsed = this._collapsed.read(reader);
return {
contentHeight: (collapsed ? 0 : this._editorContentHeight.read(reader)) + this._outerEditorHeight,
itemViewportOffset: collapsed ? 0 : this._itemViewportOffset.read(reader),
};
});
this._modifiedContentWidth = observableValue<number>(this, 0);
this._modifiedWidth = observableValue<number>(this, 0);
@@ -128,7 +136,6 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
: undefined;
this._dataStore = this._register(new DisposableStore());
this._headerHeight = 40;
this._lastScrollTop = -1;
this._isSettingScrollTop = false;
const btn = this._register(new Button(this._elements.collapseButton, {}));
@@ -204,10 +211,11 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
this._register(this.editor.onDidContentSizeChange(e => {
globalTransaction(tx => {
this._editorContentHeight.set(e.contentHeight, tx);
this._modifiedContentWidth.set(this.editor.getModifiedEditor().getContentWidth(), tx);
this._originalContentWidth.set(this.editor.getOriginalEditor().getContentWidth(), tx);
});
this._observedEditorContentHeight = e.contentHeight;
this._scheduleVerticalStateUpdate();
}));
this._register(this.editor.getOriginalEditor().onDidScrollChange(e => {
@@ -218,8 +226,8 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
if (!e.scrollTopChanged || !this._data) {
return;
}
const delta = e.scrollTop - this._lastScrollTop;
this._data.deltaScrollVertical(delta);
this._observedItemViewportOffset = e.scrollTop;
this._scheduleVerticalStateUpdate();
}));
this._register(autorun(reader => {
@@ -260,6 +268,7 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
private _data: TemplateData | undefined;
public setData(data: TemplateData | undefined): void {
this._verticalStateUpdate.clear();
this._data = data;
const optionsOverride = this._optionsOverride;
function updateOptions(options: IDiffEditorOptions): IDiffEditorOptions {
@@ -348,7 +357,6 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
private readonly _headerHeight;
private _lastScrollTop;
private _isSettingScrollTop;
public render(verticalRange: OffsetRange, width: number, editorScroll: number, viewPort: OffsetRange): void {
@@ -371,16 +379,32 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
});
try {
this._isSettingScrollTop = true;
this._lastScrollTop = editorScroll;
this._observedItemViewportOffset = editorScroll;
this.editor.getOriginalEditor().setScrollTop(editorScroll);
} finally {
this._isSettingScrollTop = false;
}
this._flushVerticalState();
this._elements.header.classList.toggle('shadow', delta > 0 || editorScroll > 0);
this._elements.header.classList.toggle('collapsed', delta === maxDelta);
}
private _scheduleVerticalStateUpdate(): void {
if (this._verticalStateUpdate.value) {
return;
}
this._verticalStateUpdate.value = scheduleAtNextAnimationFrame(getWindow(this._elements.root), () => this._flushVerticalState());
}
private _flushVerticalState(): void {
this._verticalStateUpdate.clear();
globalTransaction(tx => {
this._editorContentHeight.set(this._observedEditorContentHeight, tx);
this._itemViewportOffset.set(this._observedItemViewportOffset, tx);
});
}
public hide(): void {
this._elements.root.style.top = `-100000px`;
this._elements.root.style.visibility = 'hidden'; // Some editor parts are still visible
@@ -3,14 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension, getWindow, h, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js';
import { SmoothScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { compareBy, numberComparator } from '../../../../base/common/arrays.js';
import { findFirstMax } from '../../../../base/common/arraysFind.js';
import { Dimension, h } from '../../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';
import { IObservable, IReader, ITransaction, autorun, autorunWithStore, derived, disposableObservableValue, globalTransaction, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js';
import { Scrollable, ScrollbarVisibility } from '../../../../base/common/scrollable.js';
import { IObservable, ITransaction, autorun, autorunWithStore, derived, disposableObservableValue, observableValue, transaction } from '../../../../base/common/observable.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { ContextKeyValue, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
@@ -25,7 +21,7 @@ import { ISelection, Selection } from '../../../common/core/selection.js';
import { IDiffEditor } from '../../../common/editorCommon.js';
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
import { ICodeEditor } from '../../editorBrowser.js';
import { ObservableElementSizeObserver } from '../diffEditor/utils.js';
import { CompressedVirtualizedScrollView, ICompressedVirtualizedScrollItem } from './compressedVirtualizedScrollView.js';
import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate.js';
import { IDocumentDiffItem } from './model.js';
import { formatDiffItemKey, formatUri, ILoggedDiffItem, MultiDiffEditorLogger } from './multiDiffEditorLogging.js';
@@ -36,18 +32,10 @@ import './style.css';
import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js';
export class MultiDiffEditorWidgetImpl extends Disposable {
private readonly _scrollableElements;
private readonly _scrollable;
private readonly _scrollableElement;
private readonly _scrollView;
private readonly _elements;
private readonly _sizeObserver;
private readonly _objectPool;
private readonly _optionsOverride: IObservable<IDiffEditorOptions>;
public readonly scrollTop;
@@ -58,8 +46,6 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
private readonly _viewItems;
private readonly _spaceBetweenPx;
private readonly _totalHeight;
public readonly activeControl;
private readonly _contextKeyService;
@@ -89,85 +75,65 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
) {
super();
this._logger = this._register(new MultiDiffEditorLogger(logService));
this._scrollableElements = h('div.scrollContent', [
h('div@content', {
style: {
overflow: 'hidden',
}
}),
h('div.monaco-editor@overflowWidgetsDomNode', {
}),
]);
this._scrollable = this._register(new Scrollable({
forceIntegerValues: false,
scheduleAtNextAnimationFrame: (cb) => scheduleAtNextAnimationFrame(getWindow(this._element), cb),
smoothScrollDuration: 100,
}));
this._scrollableElement = this._register(new SmoothScrollableElement(this._scrollableElements.root, {
vertical: ScrollbarVisibility.Auto,
horizontal: ScrollbarVisibility.Auto,
useShadows: false,
}, this._scrollable));
this._elements = h('div.monaco-component.multiDiffEditor', {}, [
h('div', {}, [this._scrollableElement.getDomNode()]),
h('div.placeholder@placeholder', {}, [h('div')]),
]);
this._sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined));
this._optionsOverride = derived(this, reader => {
return { ...this._diffEditorOptions, ...this._diffLayoutOptions.read(reader) };
});
this._objectPool = this._register(new ObjectPool<TemplateData, DiffEditorItemTemplate>((data) => {
const template = this._instantiationService.createInstance(
DiffEditorItemTemplate,
this._scrollableElements.content,
this._scrollableElements.overflowWidgetsDomNode,
this._workbenchUIElementFactory,
this._optionsOverride,
);
template.setData(data);
return template;
}));
this.scrollTop = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop);
this.scrollLeft = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft);
this._viewItemsInfo = derived<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>(this,
(reader) => {
const vm = this._viewModel.read(reader);
if (!vm) {
return { items: [], getItem: _d => { throw new BugIndicatingError(); } };
}
const viewModels = vm.items.read(reader);
const map = new Map<DocumentDiffItemViewModel, VirtualizedViewItem>();
let restoredDocStates = 0;
const items = viewModels.map(d => {
const item = reader.store.add(new VirtualizedViewItem(d, this._objectPool, this.scrollLeft, delta => {
const before = this._scrollableElement.getScrollPosition().scrollTop;
this._scrollableElement.setScrollPosition({ scrollTop: before + delta });
this._logger.log('scroll adjusted by embedded editor', {
file: d.modifiedUri ?? d.originalUri,
delta,
scrollTop: `${before} -> ${this._scrollableElement.getScrollPosition().scrollTop}`,
});
}, this._logger));
const data = this._lastDocStates?.[item.getKey()];
if (data) {
restoredDocStates++;
transaction(tx => {
item.setViewState(data, tx);
});
this._spaceBetweenPx = observableValue(this, 0);
let objectPool!: ObjectPool<TemplateData, DiffEditorItemTemplate>;
let viewItemsInfo!: IObservable<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>;
let viewItems!: IObservable<readonly VirtualizedViewItem[]>;
this._scrollView = this._register(new CompressedVirtualizedScrollView(
this._element,
this._dimension,
this._spaceBetweenPx,
context => {
objectPool = this._register(new ObjectPool<TemplateData, DiffEditorItemTemplate>(data => {
const template = this._instantiationService.createInstance(
DiffEditorItemTemplate,
context.contentDomNode,
context.overflowWidgetsDomNode,
this._workbenchUIElementFactory,
this._optionsOverride,
);
template.setData(data);
return template;
}));
viewItemsInfo = derived<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>(this, reader => {
const vm = this._viewModel.read(reader);
if (!vm) {
return { items: [], getItem: _d => { throw new BugIndicatingError(); } };
}
map.set(d, item);
return item;
const map = new Map<DocumentDiffItemViewModel, VirtualizedViewItem>();
let restoredDocStates = 0;
const items = vm.items.read(reader).map(d => {
const item = reader.store.add(new VirtualizedViewItem(d, objectPool, context.scrollLeft, this._logger));
const data = this._lastDocStates?.[item.getKey()];
if (data) {
restoredDocStates++;
transaction(tx => item.setViewState(data, tx));
}
map.set(d, item);
return item;
});
this._logger.log('view items updated', {
items: items.length,
restoredDocStates,
});
return { items, getItem: d => map.get(d)! };
});
this._logger.log('view items updated', {
items: items.length,
restoredDocStates,
});
return { items, getItem: d => map.get(d)! };
}
);
this._viewItems = this._viewItemsInfo.map(this, items => items.items);
this._spaceBetweenPx = 0;
this._totalHeight = this._viewItems.map(this, (items, reader) => items.reduce((r, i) => r + i.contentHeight.read(reader) + this._spaceBetweenPx, 0));
viewItems = viewItemsInfo.map(this, items => items.items);
return viewItems;
},
));
this._viewItemsInfo = viewItemsInfo;
this._viewItems = viewItems;
this.scrollTop = this._scrollView.scrollTop;
this.scrollLeft = this._scrollView.scrollLeft;
this._elements = h('div.monaco-component.multiDiffEditor', {}, [
this._scrollView.domNode,
h('div.placeholder@placeholder', {}, [h('div')]),
]);
this.activeControl = derived(this, reader => {
const activeDiffItem = this._viewModel.read(reader)?.activeDiffItem.read(reader);
if (!activeDiffItem) { return undefined; }
@@ -211,17 +177,11 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
}
}));
this._register(autorun((reader) => {
/** @description Update widget dimension */
const dimension = this._dimension.read(reader);
this._sizeObserver.observe(dimension);
}));
this._logger.logStateChanges({
viewModel: this._viewModel,
items: this._viewItems,
spaceBetweenPx: this._spaceBetweenPx,
getScrollTop: () => this._scrollableElement.getScrollPosition().scrollTop,
spaceBetweenPx: this._spaceBetweenPx.get(),
getScrollTop: () => this._scrollView.getScrollPosition().scrollTop,
isPreserveFocusOnLoad: () => this._preserveFocusOnLoad,
});
@@ -241,32 +201,8 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
this._elements.placeholder.classList.toggle('visible', !!message);
}));
this._scrollableElements.content.style.position = 'relative';
this._register(autorun((reader) => {
/** @description Update scroll dimensions */
const height = this._sizeObserver.height.read(reader);
this._scrollableElements.root.style.height = `${height}px`;
const totalHeight = this._totalHeight.read(reader);
this._scrollableElements.content.style.height = `${totalHeight}px`;
const width = this._sizeObserver.width.read(reader);
let scrollWidth = width;
const viewItems = this._viewItems.read(reader);
const max = findFirstMax(viewItems, compareBy(i => i.maxScroll.read(reader).maxScroll, numberComparator));
if (max) {
const maxScroll = max.maxScroll.read(reader);
scrollWidth = width + maxScroll.maxScroll;
}
this._scrollableElement.setScrollDimensions({
width: width,
height: height,
scrollHeight: totalHeight,
scrollWidth,
});
this._register(autorun(reader => {
this._scrollView.scrollDimensions.read(reader);
// A restored scroll offset applied before the model updated these
// dimensions would be clamped against a stale (often 0) scrollHeight, so
// apply it here once the dimensions are known.
@@ -318,12 +254,6 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
}
}));
this._register(this._register(autorun(reader => {
/** @description Render all */
globalTransaction(tx => {
this.render(reader);
});
})));
}
public setScrollState(scrollState: { top?: number; left?: number }): void {
@@ -342,8 +272,8 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
if (!pending) {
return;
}
this._scrollableElement.setScrollPosition({ scrollLeft: pending.left, scrollTop: pending.top });
const applied = this._scrollableElement.getScrollPosition();
this._scrollView.setScrollPosition({ scrollLeft: pending.left, scrollTop: pending.top });
const applied = this._scrollView.getScrollPosition();
const topLanded = pending.top === undefined || applied.scrollTop >= pending.top;
const leftLanded = pending.left === undefined || applied.scrollLeft >= pending.left;
if (topLanded && leftLanded) {
@@ -407,15 +337,15 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
let scrollTop = 0;
for (let i = 0; i < index; i++) {
scrollTop += viewItems[i].contentHeight.get() + this._spaceBetweenPx;
scrollTop += viewItems[i].contentHeight.get() + this._spaceBetweenPx.get();
}
this._logger.log('reveal', {
file: viewItem.getLabel(),
index,
scrollTop: `${this._scrollableElement.getScrollPosition().scrollTop} -> ${scrollTop}`,
scrollTop: `${this._scrollView.getScrollPosition().scrollTop} -> ${scrollTop}`,
range: options?.range,
});
this._scrollableElement.setScrollPosition({ scrollTop });
this._scrollView.setScrollPosition({ scrollTop });
const diffEditor = viewItem.template.get()?.editor;
const editor = 'original' in resource ? diffEditor?.getOriginalEditor() : diffEditor?.getModifiedEditor();
@@ -633,40 +563,6 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
}
}
private render(reader: IReader | undefined) {
const scrollTop = this.scrollTop.read(reader);
let contentScrollOffsetToScrollOffset = 0;
let itemHeightSumBefore = 0;
let itemContentHeightSumBefore = 0;
const viewPortHeight = this._sizeObserver.height.read(reader);
const contentViewPort = OffsetRange.ofStartAndLength(scrollTop, viewPortHeight);
const width = this._sizeObserver.width.read(reader);
for (const v of this._viewItems.read(reader)) {
const itemContentHeight = v.contentHeight.read(reader);
const itemHeight = Math.min(itemContentHeight, viewPortHeight);
const itemRange = OffsetRange.ofStartAndLength(itemHeightSumBefore, itemHeight);
const itemContentRange = OffsetRange.ofStartAndLength(itemContentHeightSumBefore, itemContentHeight);
if (itemContentRange.isBefore(contentViewPort)) {
contentScrollOffsetToScrollOffset -= itemContentHeight - itemHeight;
v.hide();
} else if (itemContentRange.isAfter(contentViewPort)) {
v.hide();
} else {
const scroll = Math.max(0, Math.min(contentViewPort.start - itemContentRange.start, itemContentHeight - itemHeight));
contentScrollOffsetToScrollOffset -= scroll;
const viewPort = OffsetRange.ofStartAndLength(scrollTop + contentScrollOffsetToScrollOffset, viewPortHeight);
v.render(itemRange, scroll, width, viewPort);
}
itemHeightSumBefore += itemHeight + this._spaceBetweenPx;
itemContentHeightSumBefore += itemContentHeight + this._spaceBetweenPx;
}
this._scrollableElements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`;
}
}
function highlightRange(targetEditor: ICodeEditor, range: IRange) {
@@ -704,12 +600,15 @@ export interface IMultiDiffEditorOptionsViewState {
export type IMultiDiffResourceId = { original: URI | undefined; modified: URI | undefined };
class VirtualizedViewItem extends Disposable implements ILoggedDiffItem {
class VirtualizedViewItem extends Disposable implements ILoggedDiffItem, ICompressedVirtualizedScrollItem {
private readonly _templateRef = this._register(disposableObservableValue<IReference<DiffEditorItemTemplate> | undefined>(this, undefined));
public readonly contentHeight = derived(this, reader =>
this._templateRef.read(reader)?.object.contentHeight?.read(reader) ?? this.viewModel.lastTemplateData.read(reader).contentHeight
);
public readonly verticalState = derived(this, reader => this._templateRef.read(reader)?.object.verticalState.read(reader) ?? {
contentHeight: this.viewModel.lastTemplateData.read(reader).contentHeight,
itemViewportOffset: 0,
});
public readonly contentHeight = this.verticalState.map(this, state => state.contentHeight);
public readonly maxScroll = derived(this, reader => this._templateRef.read(reader)?.object.maxScroll.read(reader) ?? { maxScroll: 0, scrollWidth: 0 });
@@ -724,7 +623,6 @@ class VirtualizedViewItem extends Disposable implements ILoggedDiffItem {
public readonly viewModel: DocumentDiffItemViewModel,
private readonly _objectPool: ObjectPool<TemplateData, DiffEditorItemTemplate>,
private readonly _scrollLeft: IObservable<number>,
private readonly _deltaScrollVertical: (delta: number) => void,
private readonly _logger: MultiDiffEditorLogger,
) {
super();
@@ -800,7 +698,7 @@ class VirtualizedViewItem extends Disposable implements ILoggedDiffItem {
const ref = this._templateRef.get();
if (!ref) { return; }
this.viewModel.lastTemplateData.set({
contentHeight: ref.object.contentHeight.get(),
contentHeight: ref.object.verticalState.get().contentHeight,
selections: ref.object.editor.getSelections() ?? undefined,
}, tx);
}
@@ -808,7 +706,7 @@ class VirtualizedViewItem extends Disposable implements ILoggedDiffItem {
private _clear(): void {
const ref = this._templateRef.get();
if (!ref) { return; }
this._logger.log('releasing editor template', { file: this.getLabel(), contentHeight: ref.object.contentHeight.get() });
this._logger.log('releasing editor template', { file: this.getLabel(), contentHeight: ref.object.verticalState.get().contentHeight });
transaction(tx => {
this._updateTemplateData(tx);
ref.object.hide();
@@ -825,7 +723,7 @@ class VirtualizedViewItem extends Disposable implements ILoggedDiffItem {
let ref = this._templateRef.get();
if (!ref) {
ref = this._objectPool.getUnusedObj(new TemplateData(this.viewModel, this._deltaScrollVertical));
ref = this._objectPool.getUnusedObj(new TemplateData(this.viewModel));
this._templateRef.set(ref, undefined);
const selections = this.viewModel.lastTemplateData.get().selections;
@@ -32,6 +32,7 @@ export class ObjectPool<TData extends IObjectData, T extends IPooledObject<TData
dispose: () => {
this._used.delete(obj);
if (this._unused.size > 5) {
this._itemData.delete(obj);
obj.dispose();
} else {
this._unused.add(obj);
@@ -49,6 +50,7 @@ export class ObjectPool<TData extends IObjectData, T extends IPooledObject<TData
}
this._used.clear();
this._unused.clear();
this._itemData.clear();
}
}
@@ -14,7 +14,7 @@ import { buildHistoryFromTasks, renderSwimlanes } from '../../../../../base/test
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
import { createTraceLogger, ITraceLogEntry, ITraceLogger } from '../../../../../base/test/common/virtualScheduling/index.js';
import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
import { IDefaultAccountService, MANAGED_SETTINGS_FRESHNESS_NOT_REQUIRED } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
@@ -300,6 +300,8 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
managedSettingsRawResponse: null,
managedSettingsCompatibilityError: null,
onDidChangeManagedSettingsCompatibilityError: Event.None,
managedSettingsFreshness: MANAGED_SETTINGS_FRESHNESS_NOT_REQUIRED,
onDidChangeManagedSettingsFreshness: Event.None,
getDefaultAccount: async () => null,
setDefaultAccountProvider: () => { },
getDefaultAccountAuthenticationProvider: () => { return { id: 'mockProvider', name: 'Mock Provider', enterprise: false }; },
@@ -45,7 +45,7 @@ import { ContextMenuService } from '../../../platform/contextview/browser/contex
import { IContextMenuService, IContextViewDelegate, IContextViewService, IOpenContextView } from '../../../platform/contextview/browser/contextView.js';
import { ContextViewService } from '../../../platform/contextview/browser/contextViewService.js';
import { IDataChannelService, NullDataChannelService } from '../../../platform/dataChannel/common/dataChannel.js';
import { IDefaultAccountService } from '../../../platform/defaultAccount/common/defaultAccount.js';
import { IDefaultAccountService, MANAGED_SETTINGS_FRESHNESS_NOT_REQUIRED } from '../../../platform/defaultAccount/common/defaultAccount.js';
import { IConfirmation, IConfirmationResult, IDialogService, IInputResult, IPrompt, IPromptBaseButton, IPromptResult, IPromptResultWithCancel, IPromptWithCustomCancel, IPromptWithDefaultCancel } from '../../../platform/dialogs/common/dialogs.js';
import { ExtensionKind, IEnvironmentService, IExtensionHostDebugParams } from '../../../platform/environment/common/environment.js';
import { SyncDescriptor } from '../../../platform/instantiation/common/descriptors.js';
@@ -1136,6 +1136,8 @@ class StandaloneDefaultAccountService implements IDefaultAccountService {
readonly managedSettingsRawResponse: unknown = null;
readonly managedSettingsCompatibilityError = null;
readonly onDidChangeManagedSettingsCompatibilityError = Event.None;
readonly managedSettingsFreshness = MANAGED_SETTINGS_FRESHNESS_NOT_REQUIRED;
readonly onDidChangeManagedSettingsFreshness = Event.None;
async getDefaultAccount(): Promise<IDefaultAccount | null> {
return null;

Some files were not shown because too many files have changed in this diff Show More