diff --git a/.github/instructions/design-tokens.instructions.md b/.github/instructions/design-tokens.instructions.md
index 577e2c57a0a..42416f350f2 100644
--- a/.github/instructions/design-tokens.instructions.md
+++ b/.github/instructions/design-tokens.instructions.md
@@ -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`.
diff --git a/.github/skills/design-philosophy/SKILL.md b/.github/skills/design-philosophy/SKILL.md
index 69c6afa3117..2402446caf8 100644
--- a/.github/skills/design-philosophy/SKILL.md
+++ b/.github/skills/design-philosophy/SKILL.md
@@ -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).
diff --git a/.github/skills/policy-and-managed-settings/github-managed-settings.md b/.github/skills/policy-and-managed-settings/github-managed-settings.md
index ff830579601..16dfb499d8c 100644
--- a/.github/skills/policy-and-managed-settings/github-managed-settings.md
+++ b/.github/skills/policy-and-managed-settings/github-managed-settings.md
@@ -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`
diff --git a/.github/skills/policy-and-managed-settings/local-testing.md b/.github/skills/policy-and-managed-settings/local-testing.md
index 4ea7ca8d271..844bebc2a08 100644
--- a/.github/skills/policy-and-managed-settings/local-testing.md
+++ b/.github/skills/policy-and-managed-settings/local-testing.md
@@ -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`.
diff --git a/.github/skills/update-screenshots/SKILL.md b/.github/skills/update-screenshots/SKILL.md
index 4b160ad9be2..e1b6361de36 100644
--- a/.github/skills/update-screenshots/SKILL.md
+++ b/.github/skills/update-screenshots/SKILL.md
@@ -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 --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//logs > "$TMPDIR/ci-job-log.txt"
+grep -n '##\[error\]' "$TMPDIR/ci-job-log.txt"
+```
+
+Find the failed job id with:
+
+```bash
+gh pr checks --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/"
+curl -sL -o new.png "https://hediet-screenshots.azurewebsites.net/images/"
+```
+
+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
+
+```
+
+The file is generated by [`build/lib/screenshotBlocksCi.ts`](../../../build/lib/screenshotBlocksCi.ts)
+and compared **byte-for-byte**, so keep the ``
+header, the `#### ` / 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 --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:::` in the log, and download the `playwright-test-results` artifact for
+test failures.
diff --git a/.github/skills/ux-css-layout/SKILL.md b/.github/skills/ux-css-layout/SKILL.md
index 6e75d2c14cd..28fcc19190f 100644
--- a/.github/skills/ux-css-layout/SKILL.md
+++ b/.github/skills/ux-css-layout/SKILL.md
@@ -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.
diff --git a/.github/skills/ux-theming/SKILL.md b/.github/skills/ux-theming/SKILL.md
index 274f478966b..75e03630ef6 100644
--- a/.github/skills/ux-theming/SKILL.md
+++ b/.github/skills/ux-theming/SKILL.md
@@ -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%`.
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 86d22faf7e9..861b82fd214 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index aa2eef92792..17a28b4011e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/.npmrc b/.npmrc
index 6abf3010ccf..c28bfd82d97 100644
--- a/.npmrc
+++ b/.npmrc
@@ -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"
diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml
index 435fb9adf8e..6c72b85a219 100644
--- a/build/azure-pipelines/alpine/product-build-alpine.yml
+++ b/build/azure-pipelines/alpine/product-build-alpine.yml
@@ -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
diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts
index c868a15d080..e5dbc06aa94 100644
--- a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts
+++ b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts
@@ -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) {
diff --git a/build/azure-pipelines/common/listNodeModules.ts b/build/azure-pipelines/common/listNodeModules.ts
index fd894d1d68f..5ab955faca4 100644
--- a/build/azure-pipelines/common/listNodeModules.ts
+++ b/build/azure-pipelines/common/listNodeModules.ts
@@ -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');
diff --git a/build/azure-pipelines/copilot/pull-test-cache.yml b/build/azure-pipelines/copilot/pull-test-cache.yml
new file mode 100644
index 00000000000..3b2ff664402
--- /dev/null
+++ b/build/azure-pipelines/copilot/pull-test-cache.yml
@@ -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)"
diff --git a/build/azure-pipelines/copilot/test-integration-steps.yml b/build/azure-pipelines/copilot/test-integration-steps.yml
index 6049b243e82..b0d9bf1614c 100644
--- a/build/azure-pipelines/copilot/test-integration-steps.yml
+++ b/build/azure-pipelines/copilot/test-integration-steps.yml
@@ -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:
diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml
index 15630252929..4a9d5961039 100644
--- a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml
+++ b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml
@@ -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
diff --git a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml
index e5b93a11fad..fcc5663ad7c 100644
--- a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml
+++ b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml
@@ -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:
diff --git a/build/azure-pipelines/product-quality-checks.yml b/build/azure-pipelines/product-quality-checks.yml
index 2282b69ef03..59df292c8fb 100644
--- a/build/azure-pipelines/product-quality-checks.yml
+++ b/build/azure-pipelines/product-quality-checks.yml
@@ -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:
diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml
index 131f2502b1d..6a6c132da17 100644
--- a/build/azure-pipelines/web/product-build-web.yml
+++ b/build/azure-pipelines/web/product-build-web.yml
@@ -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)
diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml
index 9d49b70a9a7..01d29af19c0 100644
--- a/build/azure-pipelines/win32/sdl-scan-win32.yml
+++ b/build/azure-pipelines/win32/sdl-scan-win32.yml
@@ -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
diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml
index 721c94a8dac..4f0ee18846f 100644
--- a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml
+++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml
@@ -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
diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt
index e09dffbc721..bfb554e136b 100644
--- a/build/checksums/electron.txt
+++ b/build/checksums/electron.txt
@@ -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
diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts
index a40ff6f6db1..f0968a51bdc 100644
--- a/build/gulpfile.reh.ts
+++ b/build/gulpfile.reh.ts
@@ -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'));
diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts
index 494a3e3810a..ac122c37940 100644
--- a/build/gulpfile.vscode.ts
+++ b/build/gulpfile.vscode.ts
@@ -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'));
diff --git a/build/lib/copilot.ts b/build/lib/copilot.ts
index 3d9cfbe2adf..908bf979673 100644
--- a/build/lib/copilot.ts
+++ b/build/lib/copilot.ts
@@ -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;
diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json
index 058938ee2b8..ca81ac67daa 100644
--- a/build/lib/i18n.resources.json
+++ b/build/lib/i18n.resources.json
@@ -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"
diff --git a/build/lib/stylelint/validateDesignTokens.ts b/build/lib/stylelint/validateDesignTokens.ts
index 01c1a51ff0d..0bbdf77ce82 100644
--- a/build/lib/stylelint/validateDesignTokens.ts
+++ b/build/lib/stylelint/validateDesignTokens.ts
@@ -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' },
diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json
index 79a40ba4087..efcdff888fc 100644
--- a/build/lib/stylelint/vscode-known-variables.json
+++ b/build/lib/stylelint/vscode-known-variables.json
@@ -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",
diff --git a/build/lib/test/copilot.test.ts b/build/lib/test/copilot.test.ts
index 8e9d7ce0ddd..eaa3613f8ec 100644
--- a/build/lib/test/copilot.test.ts
+++ b/build/lib/test/copilot.test.ts
@@ -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);
diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts
index 7db8c4b5dde..eb8b42624b2 100644
--- a/build/linux/debian/dep-lists.ts
+++ b/build/linux/debian/dep-lists.ts
@@ -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)',
diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts
index 8860fd27247..d6e3e6b3230 100644
--- a/build/linux/rpm/dep-lists.ts
+++ b/build/linux/rpm/dep-lists.ts
@@ -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)',
diff --git a/cgmanifest.json b/cgmanifest.json
index 61c04cdaa60..5390edeaa3d 100644
--- a/cgmanifest.json
+++ b/cgmanifest.json
@@ -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": {
diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs
index 8f2c147fa49..4c3b40cd31a 100644
--- a/cli/src/commands/serve_web.rs
+++ b/cli/src/commands/serve_web.rs
@@ -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) {
+ 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 `/-/...`
fn get_release_from_path(path: &str, platform: Platform) -> Option<(Release, String)> {
diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs
index 18466efe027..cb9db9edaa1 100644
--- a/cli/src/tunnels/code_server.rs
+++ b/cli/src/tunnels/code_server.rs
@@ -43,6 +43,7 @@ static LISTENING_PORT_RE: LazyLock =
LazyLock::new(|| Regex::new(r"Extension host agent listening on (.+)").unwrap());
static WEB_UI_RE: LazyLock =
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 {
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::>(),
+ command
+ .get_envs()
+ .map(|(name, value)| (
+ name.to_string_lossy().into_owned(),
+ value.map(|value| value.to_string_lossy().into_owned())
+ ))
+ .collect::>(),
+ ),
+ (
+ 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()),
+ )],
+ )
+ );
+ }
+}
diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs
index f9d3594539c..71bf7ea26b1 100644
--- a/cli/src/tunnels/control_server.rs
+++ b/cli/src/tunnels/control_server.rs
@@ -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]
+ );
+ }
+}
diff --git a/extensions/copilot/src/extension/chatSessions/common/test/mockChatSessionMetadataStore.ts b/extensions/copilot/src/extension/chatSessions/common/test/mockChatSessionMetadataStore.ts
index 57280e9130b..b889c52d0f3 100644
--- a/extensions/copilot/src/extension/chatSessions/common/test/mockChatSessionMetadataStore.ts
+++ b/extensions/copilot/src/extension/chatSessions/common/test/mockChatSessionMetadataStore.ts
@@ -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';
}
diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/AGENTS.md b/extensions/copilot/src/extension/chatSessions/copilotcli/AGENTS.md
index ba766e81660..583451309a0 100644
--- a/extensions/copilot/src/extension/chatSessions/copilotcli/AGENTS.md
+++ b/extensions/copilot/src/extension/chatSessions/copilotcli/AGENTS.md
@@ -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):
diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts
index e11d25e7ebb..2836a196044 100644
--- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts
+++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts
@@ -155,6 +155,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
private readonly _sessionWorkingDirectories = new Map();
private readonly _cachedSessionItems = new Map();
private readonly _newSessionIds = new Set();
+ /** 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();
/** 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 {
+ // 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 => {
+ 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 {
+ if (isUntitledSessionId(sessionId) || this._vscodeOriginSessionIds.has(sessionId)) {
+ return false;
+ }
+ return await this._chatSessionMetadataStore.getSessionOrigin(sessionId) !== 'vscode';
+ }
+
private async shouldShowSession(sessionId: string, context?: SessionContext): Promise {
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);
diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts
index 34bdd1779ba..1c56c6a5754 100644
--- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts
+++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts
@@ -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);
diff --git a/extensions/copilot/src/extension/conversation/vscode-node/chatParticipants.ts b/extensions/copilot/src/extension/conversation/vscode-node/chatParticipants.ts
index 462f9cc5db4..36228f7fa08 100644
--- a/extensions/copilot/src/extension/conversation/vscode-node/chatParticipants.ts
+++ b/extensions/copilot/src/extension/conversation/vscode-node/chatParticipants.ts
@@ -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 => {
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);
}
diff --git a/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts
index b117816f1ff..d72bcfb0e7a 100644
--- a/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts
+++ b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts
@@ -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)}`);
diff --git a/extensions/copilot/src/extension/prompt/node/chatParticipantRequestHandler.ts b/extensions/copilot/src/extension/prompt/node/chatParticipantRequestHandler.ts
index 9b9ce443fa7..356f9cdd14b 100644
--- a/extensions/copilot/src/extension/prompt/node/chatParticipantRequestHandler.ts
+++ b/extensions/copilot/src/extension/prompt/node/chatParticipantRequestHandler.ts
@@ -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('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) {
diff --git a/extensions/copilot/src/extension/test/node/services.ts b/extensions/copilot/src/extension/test/node/services.ts
index ddac30cee47..e4cb0433394 100644
--- a/extensions/copilot/src/extension/test/node/services.ts
+++ b/extensions/copilot/src/extension/test/node/services.ts
@@ -194,5 +194,7 @@ class NullAutomodeService implements IAutomodeService {
readonly onDidChangeAutoModeTierSupport = Event.None;
+ readonly onDidRoute = Event.None;
+
invalidateRouterCache(): void { }
}
diff --git a/extensions/copilot/src/extension/tools/node/applyPatchTool.tsx b/extensions/copilot/src/extension/tools/node/applyPatchTool.tsx
index 393fdfbd2c5..e2300db42ec 100644
--- a/extensions/copilot/src/extension/tools/node/applyPatchTool.tsx
+++ b/extensions/copilot/src/extension/tools/node/applyPatchTool.tsx
@@ -116,16 +116,14 @@ export class ApplyPatchTool implements ICopilotTool {
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 {
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 {
].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 {
}
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();
@@ -291,18 +311,8 @@ export class ApplyPatchTool implements ICopilotTool {
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 {
// 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 {
}
}
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);
}
diff --git a/extensions/copilot/src/extension/tools/node/editFileToolUtils.tsx b/extensions/copilot/src/extension/tools/node/editFileToolUtils.tsx
index da68766d1fc..5d9e5cfb9d5 100644
--- a/extensions/copilot/src/extension/tools/node/editFileToolUtils.tsx
+++ b/extensions/copilot/src/extension/tools/node/editFileToolUtils.tsx
@@ -709,6 +709,8 @@ export async function applyEdit(
}
const ALWAYS_CHECKED_EDIT_PATTERNS: Readonly> = {
+ '**/.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
diff --git a/extensions/copilot/src/extension/tools/node/test/editFileToolUtils.spec.ts b/extensions/copilot/src/extension/tools/node/test/editFileToolUtils.spec.ts
index ab174f39cf8..7f19fab06f0 100644
--- a/extensions/copilot/src/extension/tools/node/test/editFileToolUtils.spec.ts
+++ b/extensions/copilot/src/extension/tools/node/test/editFileToolUtils.spec.ts
@@ -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 () => {
diff --git a/extensions/copilot/src/extension/tools/node/toolUtils.ts b/extensions/copilot/src/extension/tools/node/toolUtils.ts
index 0c2bf2931b0..5b35377140a 100644
--- a/extensions/copilot/src/extension/tools/node/toolUtils.ts
+++ b/extensions/copilot/src/extension/tools/node/toolUtils.ts
@@ -205,13 +205,13 @@ function getInstructionsIndexFile(buildPromptContext: IBuildPromptContext, custo
}
-export async function assertFileNotContentExcluded(accessor: ServicesAccessor, uri: URI, realPath?: URI): Promise {
+export async function assertFileNotContentExcluded(accessor: ServicesAccessor, uri: URI, realPath?: URI, contents?: string): Promise {
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`);
}
}
diff --git a/extensions/copilot/src/extension/tools/test/node/applyPatch/applyPatch.spec.tsx b/extensions/copilot/src/extension/tools/test/node/applyPatch/applyPatch.spec.tsx
index 510b501aa2d..07c51260eed 100644
--- a/extensions/copilot/src/extension/tools/test/node/applyPatch/applyPatch.spec.tsx
+++ b/extensions/copilot/src/extension/tools/test/node/applyPatch/applyPatch.spec.tsx
@@ -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 {
+ 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');
diff --git a/extensions/copilot/src/platform/endpoint/node/autoChatEndpoint.ts b/extensions/copilot/src/platform/endpoint/node/autoChatEndpoint.ts
index d8baababb65..89cd3a19a4c 100644
--- a/extensions/copilot/src/platform/endpoint/node/autoChatEndpoint.ts
+++ b/extensions/copilot/src/platform/endpoint/node/autoChatEndpoint.ts
@@ -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(HIDE_AUTO_EXPLAINABILITY_TREATMENT) === true;
+}
diff --git a/extensions/copilot/src/platform/endpoint/node/automodeService.ts b/extensions/copilot/src/platform/endpoint/node/automodeService.ts
index 14251fde4b7..6f087650f5d 100644
--- a/extensions/copilot/src/platform/endpoint/node/automodeService.ts
+++ b/extensions/copilot/src/platform/endpoint/node/automodeService.ts
@@ -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;
+ /**
+ * 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;
+
/**
* 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());
readonly onDidChangeAutoModeTierSupport = this._onDidChangeAutoModeTierSupport.event;
+ private readonly _onDidRoute = this._register(new Emitter());
+ 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 {
+ // 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 {
// The session this mints belongs to the account signed in right now, so
// anything resolved here is void if that account changes mid-flight.
diff --git a/extensions/copilot/src/platform/ignore/common/ignoreService.ts b/extensions/copilot/src/platform/ignore/common/ignoreService.ts
index 0f5e15ba342..8e07ad2f584 100644
--- a/extensions/copilot/src/platform/ignore/common/ignoreService.ts
+++ b/extensions/copilot/src/platform/ignore/common/ignoreService.ts
@@ -33,7 +33,7 @@ export interface IIgnoreService {
init(): Promise;
- isCopilotIgnored(file: URI, token?: CancellationToken): Promise;
+ isCopilotIgnored(file: URI, token?: CancellationToken, contents?: string): Promise;
asMinimatchPattern(): Promise;
}
diff --git a/extensions/copilot/src/platform/ignore/node/ignoreServiceImpl.ts b/extensions/copilot/src/platform/ignore/node/ignoreServiceImpl.ts
index a7c00b29acd..85e617d3d55 100644
--- a/extensions/copilot/src/platform/ignore/node/ignoreServiceImpl.ts
+++ b/extensions/copilot/src/platform/ignore/node/ignoreServiceImpl.ts
@@ -107,7 +107,7 @@ export class BaseIgnoreService implements IIgnoreService {
return this._remoteContentExclusions?.isRegexContextExclusionsEnabled ?? false;
}
- public async isCopilotIgnored(file: URI, token?: CancellationToken): Promise {
+ public async isCopilotIgnored(file: URI, token?: CancellationToken, contents?: string): Promise {
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);
}
diff --git a/extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts b/extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts
index 1bc4c8c63ba..dfb1b695f4f 100644
--- a/extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts
+++ b/extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts
@@ -173,10 +173,13 @@ export class RemoteContentExclusion implements IDisposable {
));
}
- public async isIgnored(file: URI, token: CancellationToken = CancellationToken.None): Promise {
- const memoised = this.memoisedVerdict(file);
- if (memoised !== undefined) {
- return memoised;
+ public async isIgnored(file: URI, token: CancellationToken = CancellationToken.None, contents?: string): Promise {
+ 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) {
diff --git a/extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts b/extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts
index 4f468a6025a..997bb410fbc 100644
--- a/extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts
+++ b/extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts
@@ -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'] } });
diff --git a/extensions/copilot/src/platform/networking/common/jsonBody.ts b/extensions/copilot/src/platform/networking/common/jsonBody.ts
new file mode 100644
index 00000000000..4ae0434d7c8
--- /dev/null
+++ b/extensions/copilot/src/platform/networking/common/jsonBody.ts
@@ -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);
+}
diff --git a/extensions/copilot/src/platform/networking/node/baseFetchFetcher.ts b/extensions/copilot/src/platform/networking/node/baseFetchFetcher.ts
index b7621ae704f..4af9082a4ca 100644
--- a/extensions/copilot/src/platform/networking/node/baseFetchFetcher.ts
+++ b/extensions/copilot/src/platform/networking/node/baseFetchFetcher.ts
@@ -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';
diff --git a/extensions/copilot/src/platform/networking/node/chatWebSocketManager.ts b/extensions/copilot/src/platform/networking/node/chatWebSocketManager.ts
index 133f62e3c82..a4cecd34959 100644
--- a/extensions/copilot/src/platform/networking/node/chatWebSocketManager.ts
+++ b/extensions/copilot/src/platform/networking/node/chatWebSocketManager.ts
@@ -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;
diff --git a/extensions/copilot/src/platform/networking/node/nodeFetcher.ts b/extensions/copilot/src/platform/networking/node/nodeFetcher.ts
index 5d5eb91a06c..7b0d9d7be3c 100644
--- a/extensions/copilot/src/platform/networking/node/nodeFetcher.ts
+++ b/extensions/copilot/src/platform/networking/node/nodeFetcher.ts
@@ -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';
diff --git a/extensions/copilot/src/platform/networking/test/node/jsonBody.spec.ts b/extensions/copilot/src/platform/networking/test/node/jsonBody.spec.ts
new file mode 100644
index 00000000000..e021240a01b
--- /dev/null
+++ b/extensions/copilot/src/platform/networking/test/node/jsonBody.spec.ts
@@ -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',
+ }
+ );
+ });
+});
diff --git a/extensions/copilot/src/platform/promptFiles/test/node/agentInstructionsLocator.spec.ts b/extensions/copilot/src/platform/promptFiles/test/node/agentInstructionsLocator.spec.ts
index 1d3ffc146f0..3176e2e5c7d 100644
--- a/extensions/copilot/src/platform/promptFiles/test/node/agentInstructionsLocator.spec.ts
+++ b/extensions/copilot/src/platform/promptFiles/test/node/agentInstructionsLocator.spec.ts
@@ -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`.
diff --git a/extensions/copilot/src/platform/promptFiles/vscode-node/agentInstructionsLocator.ts b/extensions/copilot/src/platform/promptFiles/vscode-node/agentInstructionsLocator.ts
index 35e8b0129ec..3ad9644181a 100644
--- a/extensions/copilot/src/platform/promptFiles/vscode-node/agentInstructionsLocator.ts
+++ b/extensions/copilot/src/platform/promptFiles/vscode-node/agentInstructionsLocator.ts
@@ -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);
diff --git a/extensions/copilot/src/util/common/test/shims/chatTypes.ts b/extensions/copilot/src/util/common/test/shims/chatTypes.ts
index 066fad29fa7..998644953fd 100644
--- a/extensions/copilot/src/util/common/test/shims/chatTypes.ts
+++ b/extensions/copilot/src/util/common/test/shims/chatTypes.ts
@@ -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;
}
}
diff --git a/extensions/markdown-language-features/markdown-editor-src/editor.ts b/extensions/markdown-language-features/markdown-editor-src/editor.ts
index 5556dc2bc3e..d2287cac278 100644
--- a/extensions/markdown-language-features/markdown-editor-src/editor.ts
+++ b/extensions/markdown-language-features/markdown-editor-src/editor.ts
@@ -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 {
diff --git a/extensions/markdown-language-features/markdown-editor-src/linkPresentationProvider.ts b/extensions/markdown-language-features/markdown-editor-src/linkPresentationProvider.ts
index 29e1e5117fe..3991b19d633 100644
--- a/extensions/markdown-language-features/markdown-editor-src/linkPresentationProvider.ts
+++ b/extensions/markdown-language-features/markdown-editor-src/linkPresentationProvider.ts
@@ -21,19 +21,19 @@ type WebviewLinkPresentation = LinkPresentation & { readonly isLoading?: boolean
export class WebviewLinkPresentationProvider extends Disposable implements ILinkPresentationProvider {
readonly #entries = new Map();
- 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,
diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json
index 7abc46a34ce..ca96b6345b8 100644
--- a/extensions/markdown-language-features/package.json
+++ b/extensions/markdown-language-features/package.json
@@ -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": [
diff --git a/extensions/markdown-language-features/src/extension.shared.ts b/extensions/markdown-language-features/src/extension.shared.ts
index ff618786901..85564580e54 100644
--- a/extensions/markdown-language-features/src/extension.shared.ts
+++ b/extensions/markdown-language-features/src/extension.shared.ts
@@ -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),
diff --git a/extensions/markdown-language-features/src/logging.ts b/extensions/markdown-language-features/src/logging.ts
index 20636643ec1..3364a06ca4b 100644
--- a/extensions/markdown-language-features/src/logging.ts
+++ b/extensions/markdown-language-features/src/logging.ts
@@ -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)] : []));
+ }
}
}
diff --git a/extensions/markdown-language-features/src/preview/linkPresentation/gitLinkPresentationResolver.ts b/extensions/markdown-language-features/src/preview/linkPresentation/gitLinkPresentationResolver.ts
index 6e6a871a303..87c1753f3e5 100644
--- a/extensions/markdown-language-features/src/preview/linkPresentation/gitLinkPresentationResolver.ts
+++ b/extensions/markdown-language-features/src/preview/linkPresentation/gitLinkPresentationResolver.ts
@@ -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 {
- return getGitCommitPresentation((await this.#findCommit(target)).commit);
+ return buildGitCommitPresentation((await this.#findCommit(target)).commit);
}
async #findCommit(target: GitCommitTarget): Promise {
@@ -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';
diff --git a/extensions/markdown-language-features/src/preview/linkPresentation/githubLinkPresentationResolver.ts b/extensions/markdown-language-features/src/preview/linkPresentation/githubLinkPresentationResolver.ts
deleted file mode 100644
index 198cad0fd63..00000000000
--- a/extensions/markdown-language-features/src/preview/linkPresentation/githubLinkPresentationResolver.ts
+++ /dev/null
@@ -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();
- readonly #authenticationSubscription: vscode.Disposable;
- #accessToken: Promise | 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 | 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 {
- 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 {
- 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 {
- 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(apiPath: string, read: (value: unknown) => T | undefined): Promise {
- 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(apiPath: string, read: (value: unknown) => readonly T[] | undefined): Promise {
- 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 {
- return typeof value === 'object' && value !== null;
-}
diff --git a/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationBuilders.ts b/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationBuilders.ts
new file mode 100644
index 00000000000..7538dd4b237
--- /dev/null
+++ b/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationBuilders.ts
@@ -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('/');
+}
diff --git a/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationResolver.ts b/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationResolver.ts
index 7b42b653ea3..c099b9cd219 100644
--- a/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationResolver.ts
+++ b/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationResolver.ts
@@ -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;
- readonly expiresAt: number;
-}
-
-interface PersistedLinkPresentationCacheEntry {
- readonly href: string;
- readonly presentation: LinkPresentation;
- readonly storedAt: number;
-}
-
-export class LinkPresentationCache {
- readonly #entries = new Map();
- readonly #persistentEntries = new Map();
- 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,
- now = Date.now(),
- ): Promise {
- 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>();
@@ -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['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 {
- return typeof value === 'object' && value !== null;
-}
diff --git a/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationService.ts b/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationService.ts
index fdd72829059..e016f09e0f5 100644
--- a/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationService.ts
+++ b/extensions/markdown-language-features/src/preview/linkPresentation/linkPresentationService.ts
@@ -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;
@@ -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 {
diff --git a/extensions/markdown-language-features/src/preview/linkPresentation/workspaceLinkPresentationResolver.ts b/extensions/markdown-language-features/src/preview/linkPresentation/workspaceLinkPresentationResolver.ts
index 2c4e64e8c64..e7252e0da98 100644
--- a/extensions/markdown-language-features/src/preview/linkPresentation/workspaceLinkPresentationResolver.ts
+++ b/extensions/markdown-language-features/src/preview/linkPresentation/workspaceLinkPresentationResolver.ts
@@ -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 {
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 {
+ 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 {
@@ -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('/');
-}
diff --git a/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts b/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts
index b3ae2f31be9..ff74f091048 100644
--- a/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts
+++ b/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts
@@ -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('experimental.richLinks.enabled', false),
+ richLinksEnabled: vscode.workspace.getConfiguration('markdown').get('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,
})),
});
diff --git a/extensions/markdown-language-features/src/preview/webviewInitialState.ts b/extensions/markdown-language-features/src/preview/webviewInitialState.ts
index b9e5474cb21..16452fd8534 100644
--- a/extensions/markdown-language-features/src/preview/webviewInitialState.ts
+++ b/extensions/markdown-language-features/src/preview/webviewInitialState.ts
@@ -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 }[];
}
/**
diff --git a/extensions/markdown-language-features/src/test/markdownEditorRichLinks.test.ts b/extensions/markdown-language-features/src/test/markdownEditorRichLinks.test.ts
index bfe6db9d73e..0da3f7f4c93 100644
--- a/extensions/markdown-language-features/src/test/markdownEditorRichLinks.test.ts
+++ b/extensions/markdown-language-features/src/test/markdownEditorRichLinks.test.ts
@@ -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();
-
- keys(): readonly string[] {
- return [...this.#values.keys()];
- }
-
- get(key: string): T | undefined;
- get(key: string, defaultValue: T): T;
- get(key: string, defaultValue?: T): T | undefined {
- return (this.#values.get(key) as T | undefined) ?? defaultValue;
- }
-
- update(key: string, value: unknown): Thenable {
- 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();
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,
diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts
index 32a93fe3646..3cc55e003e8 100644
--- a/extensions/vscode-test-resolver/src/extension.ts
+++ b/extensions/vscode-test-resolver/src/extension.ts
@@ -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
diff --git a/package-lock.json b/package-lock.json
index bb3a8ea144d..2f8c8e9990a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
],
diff --git a/package.json b/package.json
index d1bf265af0b..8eb6187847d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/remote/package-lock.json b/remote/package-lock.json
index 0d791e25e1b..8db34cdc694 100644
--- a/remote/package-lock.json
+++ b/remote/package-lock.json
@@ -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"
],
diff --git a/remote/package.json b/remote/package.json
index d53cd5c6bad..ac7a263a061 100644
--- a/remote/package.json
+++ b/remote/package.json
@@ -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",
diff --git a/scripts/mock-policy-server/README.md b/scripts/mock-policy-server/README.md
index 6af646ec933..25bc9d51563 100644
--- a/scripts/mock-policy-server/README.md
+++ b/scripts/mock-policy-server/README.md
@@ -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 |
| --- | --- | --- |
diff --git a/scripts/mock-policy-server/endpoints.ts b/scripts/mock-policy-server/endpoints.ts
index ae3c880c49f..f1bef4d0621 100644
--- a/scripts/mock-policy-server/endpoints.ts
+++ b/scripts/mock-policy-server/endpoints.ts
@@ -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' }
}
]
},
diff --git a/scripts/mock-policy-server/public/app.ts b/scripts/mock-policy-server/public/app.ts
index 1765d2a0ae5..a9c07fe31d1 100644
--- a/scripts/mock-policy-server/public/app.ts
+++ b/scripts/mock-policy-server/public/app.ts
@@ -12,6 +12,7 @@
* `endpoints.ts` (loaded via an earlier `
@@ -18,12 +18,14 @@
+
+
@@ -36,18 +38,18 @@
-
-
-
diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html
index 77881982735..dada9ce894a 100644
--- a/src/vs/code/browser/workbench/workbench.html
+++ b/src/vs/code/browser/workbench/workbench.html
@@ -2,7 +2,7 @@
-
@@ -18,6 +18,7 @@
+
@@ -33,11 +34,11 @@
-
-
diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts
index 8176a5b4bb2..c9736ff355f 100644
--- a/src/vs/code/electron-main/app.ts
+++ b/src/vs/code/electron-main/app.ts
@@ -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(NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, [url]).then(
r => callback({ ...r, data: Buffer.from(r.body, 'base64') }),
err => {
diff --git a/src/vs/editor/browser/controller/dragScrolling.ts b/src/vs/editor/browser/controller/dragScrolling.ts
index bba8a03f777..bb035d766ce 100644
--- a/src/vs/editor/browser/controller/dragScrolling.ts
+++ b/src/vs/editor/browser/controller/dragScrolling.ts
@@ -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);
diff --git a/src/vs/editor/browser/widget/multiDiffEditor/compressedVirtualizedScrollLayout.ts b/src/vs/editor/browser/widget/multiDiffEditor/compressedVirtualizedScrollLayout.ts
new file mode 100644
index 00000000000..bd88f01b1bf
--- /dev/null
+++ b/src/vs/editor/browser/widget/multiDiffEditor/compressedVirtualizedScrollLayout.ts
@@ -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}`);
+ }
+}
diff --git a/src/vs/editor/browser/widget/multiDiffEditor/compressedVirtualizedScrollView.ts b/src/vs/editor/browser/widget/multiDiffEditor/compressedVirtualizedScrollView.ts
new file mode 100644
index 00000000000..389644c865f
--- /dev/null
+++ b/src/vs/editor/browser/widget/multiDiffEditor/compressedVirtualizedScrollView.ts
@@ -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;
+ 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;
+}
+
+/**
+ * Virtualizes complete-height items into viewport-capped rows whose removed height is represented by item-local scrolling.
+ */
+export class CompressedVirtualizedScrollView extends Disposable {
+ private readonly _scrollableElements;
+ private readonly _scrollable;
+ private readonly _scrollableElement;
+ private readonly _sizeObserver;
+ private readonly _items;
+ private readonly _itemViewportPositions = new Map();
+ private _anchorItem: TItem | undefined;
+
+ readonly domNode: HTMLElement;
+ readonly scrollTop: IObservable;
+ readonly scrollLeft: IObservable;
+ readonly layout: IObservable;
+ readonly scrollDimensions: IObservable<{ readonly width: number; readonly height: number; readonly scrollWidth: number; readonly scrollHeight: number }>;
+
+ constructor(
+ elementToObserve: HTMLElement,
+ dimension: IObservable,
+ itemGap: IObservable,
+ createItems: (context: ICompressedVirtualizedScrollViewContext) => IObservable,
+ ) {
+ 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;
+ }
+}
diff --git a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts
index 0d3d4996f86..1949870f6f5 100644
--- a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts
+++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts
@@ -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;
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(this, undefined);
this._collapsed = derived(this, reader => this._viewModel.read(reader)?.collapsed.read(reader));
this._editorContentHeight = observableValue(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(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(this, 0);
this._modifiedWidth = observableValue(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
diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts
index e9e650c4606..d527b8a8f97 100644
--- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts
+++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts
@@ -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;
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((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();
- 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;
+ let viewItemsInfo!: IObservable<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>;
+ let viewItems!: IObservable;
+ this._scrollView = this._register(new CompressedVirtualizedScrollView(
+ this._element,
+ this._dimension,
+ this._spaceBetweenPx,
+ context => {
+ objectPool = this._register(new ObjectPool(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();
+ 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 | 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,
private readonly _scrollLeft: IObservable,
- 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;
diff --git a/src/vs/editor/browser/widget/multiDiffEditor/objectPool.ts b/src/vs/editor/browser/widget/multiDiffEditor/objectPool.ts
index 7bccd1cc59c..ced8b1d17d8 100644
--- a/src/vs/editor/browser/widget/multiDiffEditor/objectPool.ts
+++ b/src/vs/editor/browser/widget/multiDiffEditor/objectPool.ts
@@ -32,6 +32,7 @@ export class ObjectPool {
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(
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 }; },
diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts
index fcfbab8c024..8148cb93135 100644
--- a/src/vs/editor/standalone/browser/standaloneServices.ts
+++ b/src/vs/editor/standalone/browser/standaloneServices.ts
@@ -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 {
return null;
diff --git a/src/vs/editor/test/browser/widget/compressedVirtualizedScrollLayout.test.ts b/src/vs/editor/test/browser/widget/compressedVirtualizedScrollLayout.test.ts
new file mode 100644
index 00000000000..9305236d483
--- /dev/null
+++ b/src/vs/editor/test/browser/widget/compressedVirtualizedScrollLayout.test.ts
@@ -0,0 +1,177 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { Dimension } from '../../../../base/browser/dom.js';
+import { toDisposable } from '../../../../base/common/lifecycle.js';
+import { constObservable, IObservable, observableValue, transaction } from '../../../../base/common/observable.js';
+import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
+import { OffsetRange } from '../../../common/core/ranges/offsetRange.js';
+import { computeCompressedVirtualizedScrollLayout } from '../../../browser/widget/multiDiffEditor/compressedVirtualizedScrollLayout.js';
+import { CompressedVirtualizedScrollView, ICompressedVirtualizedScrollItem, ICompressedVirtualizedScrollItemVerticalState } from '../../../browser/widget/multiDiffEditor/compressedVirtualizedScrollView.js';
+import { Random } from '../../common/core/random.js';
+
+suite('CompressedVirtualizedScrollLayout', () => {
+ const disposables = ensureNoDisposablesAreLeakedInTestSuite();
+
+ test('describes mixed-height content in complete and rendered coordinates', () => {
+ const layout = computeCompressedVirtualizedScrollLayout({
+ scrollTop: 500,
+ viewportHeight: 400,
+ itemGap: 10,
+ itemHeights: [200, 900, 300],
+ });
+
+ assert.deepStrictEqual({
+ scrollTop: layout.scrollTop,
+ scrollHeight: layout.scrollHeight,
+ renderedHeight: layout.renderedHeight,
+ renderedScrollTop: layout.renderedViewport.start,
+ hiddenContentHeightAboveViewport: layout.hiddenContentHeightAboveViewport,
+ items: layout.items.map(item => ({
+ content: item.contentRange.toString(),
+ rendered: item.renderedRange.toString(),
+ maxScrollOffset: item.maxScrollOffset,
+ scrollOffset: item.scrollOffset,
+ visibility: item.visibility,
+ })),
+ }, {
+ scrollTop: 500,
+ scrollHeight: 1420,
+ renderedHeight: 920,
+ renderedScrollTop: 210,
+ hiddenContentHeightAboveViewport: 290,
+ items: [
+ { content: '[0, 200)', rendered: '[0, 200)', maxScrollOffset: 0, scrollOffset: 0, visibility: 'before' },
+ { content: '[210, 1110)', rendered: '[210, 610)', maxScrollOffset: 500, scrollOffset: 290, visibility: 'visible' },
+ { content: '[1120, 1420)', rendered: '[620, 920)', maxScrollOffset: 0, scrollOffset: 0, visibility: 'after' },
+ ],
+ });
+ });
+
+ test('conserves vertical displacement', () => {
+ const random = Random.create(873245);
+ const failures: string[] = [];
+
+ for (let caseIndex = 0; caseIndex < 2000; caseIndex++) {
+ const viewportHeight = random.nextIntRange(1, 1000);
+ const itemGap = random.nextIntRange(0, 41);
+ const itemHeights = Array.from(
+ { length: random.nextIntRange(0, 31) },
+ () => random.nextIntRange(0, 2501),
+ );
+ const initial = computeCompressedVirtualizedScrollLayout({
+ scrollTop: random.nextIntRange(0, 50001),
+ viewportHeight,
+ itemGap,
+ itemHeights,
+ });
+ const next = computeCompressedVirtualizedScrollLayout({
+ scrollTop: initial.scrollTop + random.nextIntRange(-2000, 2001),
+ viewportHeight,
+ itemGap,
+ itemHeights,
+ });
+
+ const scrollDelta = next.scrollTop - initial.scrollTop;
+ const renderedDelta = next.renderedViewport.start - initial.renderedViewport.start;
+ const hiddenDelta = next.hiddenContentHeightAboveViewport - initial.hiddenContentHeightAboveViewport;
+ const residual = scrollDelta - renderedDelta - hiddenDelta;
+ const invalidItem = next.items.find(item =>
+ item.scrollOffset < 0
+ || item.scrollOffset > item.maxScrollOffset
+ || item.maxScrollOffset !== Math.max(0, item.contentRange.length - item.renderedRange.length)
+ );
+
+ if (Math.abs(residual) > 0.0001 || invalidItem) {
+ failures.push(`case ${caseIndex}: residual=${residual}, invalidItem=${!!invalidItem}`);
+ if (failures.length === 10) {
+ break;
+ }
+ }
+ }
+
+ assert.deepStrictEqual(failures, []);
+ });
+
+ test('keeps the rendered anchor stable when content grows above it', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ disposables.add(toDisposable(() => container.remove()));
+
+ const itemA = new TestCompressedScrollItem(260);
+ const itemB = new TestCompressedScrollItem(1100);
+ const view = disposables.add(new CompressedVirtualizedScrollView(
+ container,
+ constObservable(new Dimension(800, 480)),
+ constObservable(12),
+ () => constObservable([itemA, itemB]),
+ ));
+ container.appendChild(view.domNode);
+ view.setScrollPosition({ scrollTop: 800 });
+
+ const getState = () => {
+ const layout = view.layout.get();
+ return {
+ scrollTop: view.getScrollPosition().scrollTop,
+ itemBViewportOffset: itemB.verticalState.get().itemViewportOffset,
+ itemBRenderedTop: layout.items[1].renderedRange.start - layout.renderedViewport.start,
+ };
+ };
+ const before = getState();
+
+ itemA.setVerticalState({ contentHeight: 360, itemViewportOffset: 0 });
+ const afterPrecedingGrowth = getState();
+ itemB.setVerticalState({ contentHeight: 1200, itemViewportOffset: 628 });
+ const afterAnchorGrowth = getState();
+
+ assert.deepStrictEqual({
+ before,
+ afterPrecedingGrowth,
+ afterAnchorGrowth,
+ }, {
+ before: {
+ scrollTop: 800,
+ itemBViewportOffset: 528,
+ itemBRenderedTop: 0,
+ },
+ afterPrecedingGrowth: {
+ scrollTop: 900,
+ itemBViewportOffset: 528,
+ itemBRenderedTop: 0,
+ },
+ afterAnchorGrowth: {
+ scrollTop: 1000,
+ itemBViewportOffset: 628,
+ itemBRenderedTop: 0,
+ },
+ });
+ });
+});
+
+class TestCompressedScrollItem implements ICompressedVirtualizedScrollItem {
+ readonly verticalState;
+ readonly maxScroll: IObservable<{ readonly maxScroll: number }> = constObservable({ maxScroll: 0 });
+
+ constructor(contentHeight: number) {
+ this.verticalState = observableValue(this, {
+ contentHeight,
+ itemViewportOffset: 0,
+ });
+ }
+
+ setVerticalState(state: ICompressedVirtualizedScrollItemVerticalState): void {
+ transaction(tx => this.verticalState.set(state, tx));
+ }
+
+ render(_renderedRange: OffsetRange, scrollOffset: number, _width: number, _renderedViewport: OffsetRange): void {
+ const state = this.verticalState.get();
+ if (state.itemViewportOffset !== scrollOffset) {
+ this.setVerticalState({ ...state, itemViewportOffset: scrollOffset });
+ }
+ }
+
+ hide(): void { }
+}
diff --git a/src/vs/editor/test/browser/widget/objectPool.test.ts b/src/vs/editor/test/browser/widget/objectPool.test.ts
new file mode 100644
index 00000000000..0e016dadd94
--- /dev/null
+++ b/src/vs/editor/test/browser/widget/objectPool.test.ts
@@ -0,0 +1,93 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { Disposable } from '../../../../base/common/lifecycle.js';
+import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
+import { IObjectData, IPooledObject, ObjectPool } from '../../../browser/widget/multiDiffEditor/objectPool.js';
+
+suite('MultiDiffEditorObjectPool', () => {
+ const disposables = ensureNoDisposablesAreLeakedInTestSuite();
+
+ test('rebinds an unused object to new data', () => {
+ let nextObjectId = 1;
+ const pool = disposables.add(new ObjectPool(data => new TestObject(nextObjectId++, data)));
+ const first = pool.getUnusedObj(new TestData('A'));
+ const second = pool.getUnusedObj(new TestData('B'));
+ const firstObject = first.object;
+ first.dispose();
+
+ const rebound = pool.getUnusedObj(new TestData('C'));
+
+ assert.deepStrictEqual({
+ reusedFirstObject: rebound.object === firstObject,
+ objectId: rebound.object.id,
+ dataId: rebound.object.data.id,
+ setDataCalls: rebound.object.setDataCalls,
+ }, {
+ reusedFirstObject: true,
+ objectId: 1,
+ dataId: 'C',
+ setDataCalls: 1,
+ });
+
+ second.dispose();
+ rebound.dispose();
+ });
+
+ test('disposes objects beyond the unused cache limit', () => {
+ const objects: TestObject[] = [];
+ const pool = disposables.add(new ObjectPool(data => {
+ const object = new TestObject(objects.length + 1, data);
+ objects.push(object);
+ return object;
+ }));
+ const references = Array.from({ length: 8 }, (_, index) => pool.getUnusedObj(new TestData(String(index))));
+
+ for (const reference of references) {
+ reference.dispose();
+ }
+ const disposedAfterRelease = objects.filter(object => object.disposed).length;
+ pool.dispose();
+
+ assert.deepStrictEqual({
+ disposedAfterRelease,
+ disposedAfterPoolDisposal: objects.filter(object => object.disposed).length,
+ }, {
+ disposedAfterRelease: 2,
+ disposedAfterPoolDisposal: 8,
+ });
+ });
+});
+
+class TestData implements IObjectData {
+ constructor(readonly id: string) { }
+
+ getId(): unknown {
+ return this.id;
+ }
+}
+
+class TestObject extends Disposable implements IPooledObject {
+ setDataCalls = 0;
+ disposed = false;
+
+ constructor(
+ readonly id: number,
+ public data: TestData,
+ ) {
+ super();
+ }
+
+ setData(data: TestData): void {
+ this.data = data;
+ this.setDataCalls++;
+ }
+
+ override dispose(): void {
+ this.disposed = true;
+ super.dispose();
+ }
+}
diff --git a/src/vs/platform/agentHost/AGENTS.md b/src/vs/platform/agentHost/AGENTS.md
index 74d1c44cfef..dc7705b493b 100644
--- a/src/vs/platform/agentHost/AGENTS.md
+++ b/src/vs/platform/agentHost/AGENTS.md
@@ -119,8 +119,11 @@ Agents do **not** maintain the chat catalog, persist membership, know whether a
### Orchestrator layer
**`AgentService` (`node/agentService.ts`):**
-- Owns the `(session, chat)` → `(agent, session URI, chat URI)` mapping.
-- Owns `_providers`, `_sessionToProvider`, and `_findProviderForSession` (which falls back through the session URI's scheme when a session was restored without an `AgentService.createSession` call in this process lifetime).
+- Resolves the `(session, chat)` → `(agent, session URI, chat URI)` mapping for
+ orchestration.
+- Uses `IAgentHostProviderService` for provider ownership and session routing. Its
+ `getProviderForSession` path falls back through the session URI's scheme when
+ a restored session was not associated in this process lifetime.
- Owns `AgentSessionRegistry`, the durable source of truth for which sessions exist. `listSessions` enumerates the registry, hydrates each initial chat through `IAgent.getChatMetadata`, and applies the existing DB/state overlays.
- Dispatches user-driven chat lifecycle (`createChat`, `disposeChat`) to `chats.*`.
- Disposes every catalog chat in stable order (peers first, initial chat last); releases every catalog chat on idle eviction.
@@ -187,13 +190,18 @@ A session URI (`ahp-copilot://`, `ahp-claude://`, …) identifies a session. A c
The default chat URI is derived from the AH session URI, but its provider identity is opaque `providerData`. Claude and Copilot mint independent SDK ids, return them from `createChat`, and restore them through `materializeChat`; equality with the AH session id is never assumed and there is no identity-reuse bind fallback. Codex persists its explicit thread mapping. AH never depends on provider identity reuse for ownership or enumeration.
**I4 — Single catalog path (spawn channel).**
-Both user-driven chats (`AgentService.createChat` → `addChat`) and harness-spawned chats (`AgentService._onChatSpawned` → `addChat`) go through `AgentHostStateManager.addChat`. The spawn-channel listener is registered **before** `AgentSideEffects` during `registerProvider` (`node/agentService.ts:registerProvider`) to guarantee the chat exists in the catalog before any turn actions arrive for it (DR1 deterministic sequencing).
+Both user-driven chats (`AgentService.createChat` → `addChat`) and harness-spawned chats (`AgentService._onChatSpawned` → `addChat`) go through `AgentHostStateManager.addChat`. `AgentService` installs the spawn-channel listener **before** the `AgentSideEffects` listener through the provider service's synchronous initializer to guarantee the chat exists in the catalog before any turn actions arrive for it (DR1 deterministic sequencing).
**I5 — Orchestrator peer-chat catalog is the restore source of truth (with one-time legacy migration).**
The orchestrator persists additional chats in `PEER_CHATS_METADATA_KEY` and the initial chat's opaque backing in `defaultChatProviderData`. Restore materializes both through the same provider-data contract — `materializeChat` is the *only* way a default chat is re-attached. When a native catalog session has no persisted blob, the provider recovers its backing from the provider-native session id in the Agent Host session URI and returns canonical provider data, which the host persists additively for later restores; an already-canonical blob is never rewritten. A missing additional-chat catalog triggers the one-time `listLegacyChatBackings` migration. Harness-spawned chats remain transient and are re-derived from tool-origin state. `_persistDefaultChatBacking`'s two writes — the `defaultChatProviderData` blob and the default chat's own `_markChatBacking` call (I7) — are independent: a failure persisting the blob is logged and swallowed rather than skipping the backing marker, since the marker is what keeps the default chat's backing session out of the top-level list and must not be held hostage to an unrelated write's success.
-**I6 — `_findProviderForSession` not `_sessionToProvider`.**
-The `_sessionToProvider` map is populated only by `AgentService.createSession`. A restored session (alive in the state manager after a host restart but never created in this process) is absent from it. `_findProviderForSession` (`node/agentService.ts:AgentService._findProviderForSession`) falls back to the session URI scheme, which is what makes restored sessions work.
+**I6 — Route through `IAgentHostProviderService`.**
+The provider service's explicit session association is populated only by
+`AgentService.createSession`. A restored session (alive in the state manager
+after a host restart but never created in this process) is absent from it, so
+restore re-associates the durable `AgentSessionRegistry` provider before
+lookup. For unregistered provider-native sessions, `getProviderForSession`
+falls back to the session URI scheme. Do not read the association map directly.
**I7 — A peer chat's backing SDK session must never surface as a top-level session.**
Some agents store all SDK conversations in one catalog. `IAgentCreateChatResult.backingSession` lets the orchestrator mark any internal chat backing, including the default Claude backing, so continual external-chat discovery never registers it as a top-level AH session. Providers own native enumeration and push candidates through `onDidDiscoverChats`; Agent Host reconciles those candidates against its registry and suppresses separately enumerable internal backings. Existing AH-created rows retain their provenance. Marking a backing session is a durable metadata write on the backing session's own DB (`_markChatBacking`); a transient failure is retried once, and if it keeps failing the session is suppressed from listing/discovery in-process (`_unpersistedChatBackings`) rather than failing the chat creation that triggered it.
@@ -224,7 +232,7 @@ If a provider cannot enumerate yet, its initial discovery attempt emits nothing;
`listSessions()` coalesces concurrent computations per external-sessions mode, so the burst of calls a multi-window restore produces shares one registry traversal instead of one per window. The shared entry records the registry epoch it started at and is invalidated by every registry mutation. A computation whose epoch changes restarts against the new registry, so both existing and later callers receive a complete post-mutation snapshot; each caller receives its own array.
-Legacy registry migration uses the `listChatsToMigrate()` contract. An array is authoritative even when empty, while `undefined` means the catalog is unavailable and must not advance migration markers. Agent Service retries an unavailable registration-time catalog once before listing; persistent unavailability rejects the aggregate `listSessions()` call with a typed provider-catalog error so clients preserve their last successful snapshots. `BaseAgentHostSessionsProvider` retries failures with exponential backoff; `AgentHostSessionListStore` leaves its cache invalid and retries on the next controller, lifecycle, or workspace refresh trigger. Replacement retry ownership is compare-and-swap single-flight: overlapping list computations that observed the same failed attempt await the first caller's installed retry rather than queueing another provider enumeration. Successful providers retain their completed migration state when a sibling provider is unavailable.
+Legacy registry migration uses the `listChatsToMigrate()` contract. An array is authoritative even when empty. `undefined` means the catalog is unavailable and must not advance migration markers; Agent Service retries an unavailable registration-time catalog once before listing, and persistent unavailability rejects aggregate `listSessions()` with a typed provider-catalog error so clients preserve their last successful snapshots. `AgentChatMigrationDeferred` means the catalog cannot be enumerated until an external readiness action, such as downloading an optional SDK: it does not advance the provider marker and does not block healthy providers' aggregate listing. A provider's later discovery signal force-retries its migration partition before additively registering the signal's unknown/external entries, and a subsequent list refresh can retry a still-deferred provider. `BaseAgentHostSessionsProvider` retries failures with exponential backoff; `AgentHostSessionListStore` leaves its cache invalid and retries on the next controller, lifecycle, or workspace refresh trigger. Replacement retry ownership is compare-and-swap single-flight: overlapping list computations that observed the same failed attempt await the first caller's installed retry rather than queueing another provider enumeration. Successful providers retain their completed migration state when a sibling provider is unavailable.
Session-list clients treat only a successful return as authoritative. `BaseAgentHostSessionsProvider` and `AgentHostSessionListStore` retain their last successful snapshots when `listSessions()` rejects; a successful empty array still clears the snapshot. This separation prevents transport, authentication, or catalog failures from becoming deletion deltas.
@@ -232,31 +240,21 @@ Provider-private discovery helpers name their concrete source: Claude uses `_lis
For every provider, migration and discovery partition the same native catalog: migration returns known entries as plain metadata, while discovery emits unknown entries with provider-classified provenance (external for Claude and Codex, and for Copilot everything except an unknown legacy extension-host chat, which is emitted as internal and adoptable). The partition is not quite exhaustive for Copilot: a chat whose session database exists but holds none of the metadata keys `listChatsToMigrate` requires is rejected by both halves. That is deliberate — an empty database is how Agent Host records a chat it already touched — and is asserted by `copilotAgent.test.ts`'s "does not discover an extension-host chat with an empty Agent Host database". Central `agent-host.db` remains the durable provenance authority.
-### Server-tool orchestration relationships
+### Server-tool creation provenance
-Treat a session as the user-visible unit of work. The `create_chat` tool is the
-default for parallel subtasks that should share one workspace, lifecycle, and
-aggregate diff. Use `create_session` only when a delegated task needs an
-independent workspace, worktree or branch, provider, or lifecycle.
+Treat a session as the user-visible unit of work. `create_session` requires a
+relationship: `currentSession` creates a peer chat for tasks in the current plan
+or deliverable, sharing its workspace, lifecycle, and aggregate diff;
+`independent` creates a top-level session for a separate deliverable that needs
+its own workspace, provider, or lifecycle. A title is required for both
+relationships and is applied before the initial prompt starts.
-Sessions created by the `create_session` server tool record provider-neutral
-orchestration metadata in the session summary `_meta` bag. The metadata names
-the creating session separately from the hierarchy parent, plus an optional
-label, whether the child may coordinate with its creator, and an optional
-idle-notification policy. Keeping creator identity separate from hierarchy
-placement preserves notification routing if parent relationships evolve.
-`list_sessions` projects and filters hierarchy metadata without involving
-provider harnesses.
-
-`SessionCoordinationService` owns idle-notification status observation,
-per-child sequencing, creator restoration, and delivery. Its durable
-`creatorNotificationState` is `waitingForCompletion` after work starts and
-`notified` after the next input-needed/idle/error transition wakes the creator.
-The `always` policy returns to `waitingForCompletion` on the next work cycle. A
-busy creator default chat receives a queued system notification rather than a
-new active turn, so concurrent child completion cannot overwrite creator work.
-The existing pending-message drain starts that queued notification when the
-creator chat becomes idle.
+Sessions created by the `create_session` server tool record only the creating
+session, chat, and turn as immutable, provider-neutral creation provenance in
+the initial session summary `_meta` bag, before the session is published or its
+first prompt starts. The reference supports related-session placement,
+source identification and session-list presentation; it does not define a
+hierarchy, grant communication privileges, or trigger lifecycle notifications.
`list_sessions` exposes a session's configured project URI separately from its
primary and additional working directories. `create_session` accepts those URIs
@@ -332,11 +330,12 @@ graph LR
sequenceDiagram
participant UI as Sessions UI
participant AS as AgentService
+ participant PS as AgentHostProviderService
participant A as IAgent.chats
participant SM as AgentHostStateManager
UI->>AS: createChat(session, chatUri, options?)
- AS->>AS: _findProviderForSession(session)
+ AS->>PS: getProviderForSession(session)
AS->>A: chats.createChat(chatUri, session, convOptions)
A-->>AS: IAgentCreateChatResult { providerData?, backingSession? }
AS->>SM: addChat(session, chatUri, { providerData })
@@ -444,7 +443,7 @@ graph TD
B{isAhpChatChannel?}
C["chatChannel = channel\nsessionChannel = parseRequiredSessionUriFromChatUri(channel)"]
D["sessionChannel = channel\nchatChannel = undefined"]
- E["agent = _findProviderForSession(sessionChannel)"]
+ E["agent = providerService.getProviderForSession(sessionChannel)"]
F["session = sessionChannel (session URI)\nchat = chatChannel (concrete chat channel URI)"]
A --> B
B -->|yes| C
diff --git a/src/vs/platform/agentHost/browser/agentHostProtocolClient.ts b/src/vs/platform/agentHost/browser/agentHostProtocolClient.ts
index 762d965b188..4d0cb55cfcf 100644
--- a/src/vs/platform/agentHost/browser/agentHostProtocolClient.ts
+++ b/src/vs/platform/agentHost/browser/agentHostProtocolClient.ts
@@ -18,7 +18,7 @@ import { FileSystemProviderErrorCode, toFileSystemProviderErrorCode } from '../.
import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService } from '../../configuration/common/configuration.js';
import { AgentSession, IAgentCreateChatRequestOptions, IAgentCreateSessionConfig, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IMcpNotification } from '../common/agent.js';
import { AGENT_HOST_DEBUG_LOGS_CHUNK_BYTES, AGENT_HOST_DEBUG_LOGS_MAX_ENTRIES, IAgentConnection, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult, type AgentHostDebugLogsArtifactKind, type IAgentHostDebugLogsArtifact, type IAgentHostDebugLogsChunk } from '../common/agentService.js';
-import { CollectAgentHostDebugLogsExtensionMethod, GetAgentHostSessionStateFileExtensionMethod, ReadAgentHostDebugLogsChunkExtensionMethod, type IAgentHostExtensionCommandMap } from '../common/agentHostExtensionProtocol.js';
+import { CollectAgentHostDebugLogsExtensionMethod, GetAgentHostSessionStateFileExtensionMethod, ReadAgentHostDebugLogsChunkExtensionMethod, supportsAgentHostChatStateFile, type IAgentHostExtensionCommandMap, type IAgentHostExtensionInitializeResult } from '../common/agentHostExtensionProtocol.js';
import { AMBIENT_AGENT_HOST_AUTHORITY } from '../common/agentHostConnectionsService.js';
import { createRemoteWatchHandle, type IRemoteWatchHandle } from '../common/agentHostFileSystemProvider.js';
import { AgentSubscriptionManager, type IActiveSubscriptionInfo, type IAgentSubscription } from '../common/state/agentSubscription.js';
@@ -26,7 +26,8 @@ import { agentHostAuthority, createAgentHostResourceUriMapper, fromAgentHostUri,
import { AgentHostResourceIdentity, AgentHostResourcePermissionError, IAgentHostResourceService, LOCAL_AGENT_HOST_RESOURCE_IDENTITY } from '../common/agentHostResourceService.js';
import type { ClientNotificationMap, CommandMap, JsonRpcErrorResponse, JsonRpcRequest } from '../common/state/protocol/messages.js';
import { ActionType, type ActionEnvelope, type ChatAction, type ClientAnnotationsAction, type ClientChangesetAction, type INotification, type IRootConfigChangedAction, type SessionAction, type TerminalAction } from '../common/state/sessionActions.js';
-import { MessageAttachmentKind, SessionSummary, ROOT_STATE_URI, StateComponents, isAhpRootChannel, type ClientPluginCustomization, type Message, type RootState } from '../common/state/sessionState.js';
+import { MessageAttachmentKind, SessionSummary, ROOT_STATE_URI, StateComponents, isAhpRootChannel, isDefaultChatUri, type ClientPluginCustomization, type Message, type RootState } from '../common/state/sessionState.js';
+import { normalizeLegacyActionEnvelope } from '../common/state/legacyProtocolCompatibility.js';
import { SUPPORTED_PROTOCOL_VERSIONS } from '../common/state/protocol/version/registry.js';
import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, ProtocolError, ReconnectResultType, type ProtocolMessage, type IStateSnapshot } from '../common/state/sessionProtocol.js';
import { type IVscodeUpgradeResult } from '../common/state/protocolUpgrade.js';
@@ -45,7 +46,6 @@ import { AgentHostClientConnectionKind, toAgentHostClientMeta } from '../common/
import type { OtlpExportLogsParams } from '../common/state/protocol/channels-otlp/notifications.js';
import type { TelemetryCapabilities } from '../common/state/protocol/channels-otlp/state.js';
import type { Implementation, InitializeResult } from '../common/state/protocol/common/commands.js';
-import { dirname } from '../../../base/common/resources.js';
import { observableValue, type IObservable } from '../../../base/common/observable.js';
import { isFileResourceRead } from '../common/resourceReadLogging.js';
import { ResourceSet } from '../../../base/common/map.js';
@@ -192,7 +192,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
* {@link connect} and re-captured after a soft-reconnect that pulled
* a fresh snapshot. `undefined` before the handshake completes.
*/
- private readonly _initializeResult = observableValue('agentHostInitializeResult', undefined);
+ private readonly _initializeResult = observableValue('agentHostInitializeResult', undefined);
private readonly _subscriptionManager: AgentSubscriptionManager;
private readonly _onDidAction = this._register(new Emitter());
@@ -456,7 +456,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
throw transportLostError(this._address);
}
- const result = await this._dispatchRequest('initialize', {
+ const result = await this._dispatchRequest('initialize', {
channel: ROOT_STATE_URI,
// Advertise every version this client can negotiate, most-preferred first, so an
// older host (a cloud sandbox running a 0.5.x `copilotd`) can negotiate down
@@ -727,7 +727,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
}
this._logService.info(`[RemoteAgentHostProtocol] Server forgot client ${this._clientId}; initializing a fresh connection.`);
- const initializeResult = await this._dispatchRequest('initialize', {
+ const initializeResult = await this._dispatchRequest('initialize', {
channel: ROOT_STATE_URI,
protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
clientId: this._clientId,
@@ -797,7 +797,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
);
}
- private _applyInitializeResult(result: CommandMap['initialize']['result'], forwardClientConfig = true): void {
+ private _applyInitializeResult(result: IAgentHostExtensionInitializeResult, forwardClientConfig = true): void {
this._initializeResult.set(result, undefined);
this._serverSeq = result.serverSeq;
if (result.defaultDirectory) {
@@ -862,7 +862,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
if (envelope.serverSeq > maxSeq) {
maxSeq = envelope.serverSeq;
}
- this._onDidAction.fire(envelope);
+ this._onDidAction.fire(normalizeLegacyActionEnvelope(envelope));
}
this._serverSeq = maxSeq;
if (result.missing.length > 0) {
@@ -1133,9 +1133,14 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
return this._sendExtensionRequest('getManagedSettingsDiagnostics');
}
- async getSessionStateFile(session: URI): Promise {
+ async getSessionStateFile(session: URI, chat?: URI): Promise {
+ const targetChat = chat && !isDefaultChatUri(chat) ? chat : undefined;
+ if (targetChat && !supportsAgentHostChatStateFile(this._initializeResult.get())) {
+ return undefined;
+ }
const result = await this._sendExtensionRequest(GetAgentHostSessionStateFileExtensionMethod, {
session: session.toString(),
+ chat: targetChat?.toString(),
});
if (!result.resource) {
return undefined;
@@ -1346,7 +1351,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
} catch {
continue;
}
- this._grantImplicitRead(dirname(uri));
+ this._grantImplicitRead(uri);
}
}
@@ -1471,7 +1476,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
// Protocol envelope → VS Code envelope (superset of action types)
const envelope = msg.params;
this._serverSeq = Math.max(this._serverSeq, envelope.serverSeq);
- this._onDidAction.fire(envelope);
+ this._onDidAction.fire(normalizeLegacyActionEnvelope(envelope));
break;
}
case 'root/sessionAdded':
diff --git a/src/vs/platform/agentHost/common/agent.ts b/src/vs/platform/agentHost/common/agent.ts
index c5653860ae4..4de8d0e4ae6 100644
--- a/src/vs/platform/agentHost/common/agent.ts
+++ b/src/vs/platform/agentHost/common/agent.ts
@@ -140,6 +140,14 @@ export interface IAgentChatMetadata {
readonly _meta?: SessionMeta;
}
+/** Identifies metadata reads that may initialize an otherwise lazy provider. */
+export interface IAgentChatMetadataOptions {
+ /** A session restore needs authoritative provider data and may start its runtime. */
+ readonly activation?: 'restore';
+ /** Stable host-owned timestamps a lazy provider may use for passive catalogue metadata. */
+ readonly registryFallback?: Pick;
+}
+
/** A provider chat ready to be registered as an Agent Host session. */
export interface IAgentDiscoveredChat extends IAgentChatMetadata {
readonly external: boolean;
@@ -616,6 +624,12 @@ export interface IAgentLegacyChat {
readonly providerData?: string;
}
+/** The native chat catalog requires an external readiness action before it can be enumerated. */
+export const AgentChatMigrationDeferred = Symbol('AgentChatMigrationDeferred');
+
+/** Provider-native chat catalog result used by registry migration. */
+export type AgentChatMigrationResult = readonly IAgentChatMetadata[] | undefined | typeof AgentChatMigrationDeferred;
+
/**
* Identifies the parent that spawned a chat. The orchestrator records
* it as the spawned chat's {@link ChatOriginKind.Tool} origin so clients can
@@ -740,6 +754,9 @@ export interface IAgentChats {
*/
sendMessage(chat: URI, prompt: string, workingDirectoriesOrDirectory: readonly URI[] | URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string, clientTypeOrContext?: AgentHostClientType | URI | IAgentChatContext, context?: URI | IAgentChatContext): Promise;
+ /** Resume a failed turn without adding another user message. */
+ resumeTurn?(chat: URI, turnId: string, context: AgentChatOperationContext, senderClientId?: string, clientType?: AgentHostClientType): Promise;
+
/** Abort the in-flight turn for `chat`. */
abort(chat: URI, context: AgentChatOperationContext): Promise;
@@ -858,7 +875,7 @@ export interface IAgentToolPendingConfirmationSignal {
/** Protocol-shaped pending-confirmation state, dispatched verbatim into `ChatToolCallReady`. */
readonly state: ToolCallPendingConfirmationState;
/** Host-only auto-approval kind (not part of the dispatched action). */
- readonly permissionKind?: 'shell' | 'write' | 'mcp' | 'read' | 'url' | 'skill' | 'custom-tool' | 'hook' | 'memory' | 'factory' | 'extension-management' | 'extension-permission-access';
+ readonly permissionKind?: 'shell' | 'write' | 'mcp' | 'read' | 'url' | 'skill' | 'custom-tool' | 'hook' | 'memory' | 'factory' | 'extension-management' | 'extension-permission-access' | 'extension-env-access';
/** Host-only auto-approval path target (not part of the dispatched action). */
readonly permissionPath?: string;
/**
@@ -1195,16 +1212,16 @@ export interface IAgent {
/** Optional recovery hook for providers with historical backings but no persisted provider data. */
recoverLegacyChat?(chat: URI, context: URI | IAgentChatContext): Promise;
- /** Enumerate provider-native chats for registry migration; `undefined` means the catalog is unavailable. */
- listChatsToMigrate(): Promise;
+ /** Enumerate provider-native chats for registry migration. */
+ listChatsToMigrate(): Promise;
/** Optional migration codec for providers that persisted peer backings before the host catalog. */
listLegacyChatBackings?(configurationResource: URI): Promise;
// ---- Metadata -----------------------------------------------------------
- /** Retrieve metadata for an exact registered chat. */
- getChatMetadata(chat: URI, context: URI | IAgentChatContext, providerData?: string): Promise;
+ /** Retrieve metadata for an exact registered chat. Ambient catalogue reads never set {@link IAgentChatMetadataOptions.activation}. */
+ getChatMetadata(chat: URI, context: URI | IAgentChatContext, providerData?: string, options?: IAgentChatMetadataOptions): Promise;
// ---- Authentication and diagnostics ------------------------------------
@@ -1229,7 +1246,7 @@ export interface IAgent {
getManagedSettingsDiagnostics?(): Promise;
/** Return the provider-owned state file for a session, when one exists. */
- getSessionStateFile?(session: URI): Promise;
+ getSessionStateFile?(session: URI, chat?: URI): Promise;
/** Add provider-owned diagnostics to an Agent Host debug-log staging directory. */
collectDebugLogs?(session: URI | undefined, outputDirectory: URI, chat?: URI): Promise;
diff --git a/src/vs/platform/agentHost/common/agentHostChatContributionsService.ts b/src/vs/platform/agentHost/common/agentHostChatContributionsService.ts
index 9bf423634ad..b697fa4c0b4 100644
--- a/src/vs/platform/agentHost/common/agentHostChatContributionsService.ts
+++ b/src/vs/platform/agentHost/common/agentHostChatContributionsService.ts
@@ -8,7 +8,7 @@ import type { ISettableObservable } from '../../../base/common/observable.js';
import type { StopWatch } from '../../../base/common/stopwatch.js';
import { createDecorator, type BrandedService } from '../../instantiation/common/instantiation.js';
import type { IAgent } from './agent.js';
-import type { AgentHostLaunchKind, IAgentHostClientTelemetryContext } from './agentHostTelemetry.js';
+import type { AgentHostLaunchKind, AgentHostTurnFailureStage, IAgentHostClientTelemetryContext } from './agentHostTelemetry.js';
import type { StateAction } from './state/sessionActions.js';
import type { ErrorInfo, Message, Turn, URI as ProtocolURI } from './state/sessionState.js';
@@ -23,7 +23,7 @@ export const IAgentHostChatContributions = createDecorator;
+ /**
+ * Decides whether a turn request may proceed to its provider. Contributions run
+ * in `order` and the first non-accept disposition wins; later contributions are
+ * not consulted.
+ *
+ * Unlike every other hook, this one fails CLOSED: a contribution that throws
+ * rejects the request rather than being skipped. It is an admission gate, so
+ * treating a failure as an accept would let a request past a guard that exists
+ * to stop it.
+ *
+ * Deliberately synchronous. A gate must decide before the send path performs any
+ * await, so the state it reads cannot change under it between the decision and
+ * its effect. Every admission check the host performs today — read-only and
+ * archived chats, local commands, a missing provider — is synchronous.
+ */
+ onIncomingRequest?(request: IIncomingRequest): IncomingRequestDisposition | undefined;
/**
* Hydrates the complete restored turn list. Each ordered stage receives the previous stage's output;
* failures preserve that previous list so a failed enrichment never loses chat history.
*/
onHydrateTurns?(context: IHydrationContext, turns: readonly Turn[]): readonly Turn[] | Promise;
+ /**
+ * Hydrates host-owned chat state before the chat is registered in the
+ * session catalog. Each ordered stage receives the previous stage's result;
+ * failures preserve that previous value, so a failed enrichment never drops
+ * an already-restored title or draft.
+ */
+ onHydrateChat?(context: IHydrationContext, restored: IRestoredChat): IRestoredChat | Promise;
}
export type IAgentHostChatContributionSignature = new (context: IAgentHostChatContributionContext, ...services: Services) => IAgentHostChatContribution;
@@ -204,7 +273,9 @@ export interface IAgentHostChatContributions extends IDisposable {
turnEnd(turn: ITurnEnd): void;
action(action: IObservedAction): void;
outgoingTurn(turn: IOutgoingTurn): Promise;
+ incomingRequest(request: IIncomingRequest): IncomingRequestDisposition;
hydrateTurns(context: IHydrationContext, turns: readonly Turn[]): Promise;
+ hydrateChat(context: IHydrationContext, restored: IRestoredChat): Promise;
disposeChatState(chat: ProtocolURI): void;
disposeSessionState(session: ProtocolURI): void;
}
diff --git a/src/vs/platform/agentHost/common/agentHostConversationContext.ts b/src/vs/platform/agentHost/common/agentHostConversationContext.ts
index b774fe03746..31df2a2fb0f 100644
--- a/src/vs/platform/agentHost/common/agentHostConversationContext.ts
+++ b/src/vs/platform/agentHost/common/agentHostConversationContext.ts
@@ -96,3 +96,14 @@ export function truncateMiddle(text: string, maxChars: number): string {
const tail = keep - head;
return `${text.slice(0, head)}${marker}${text.slice(text.length - tail)}`;
}
+
+/** Returns the last turn that is not host-injected local context. */
+export function resolveLastNonLocalTurnId(turns: readonly Turn[], isLocal: (turnId: string) => boolean): string | undefined {
+ for (let i = turns.length - 1; i >= 0; i--) {
+ const turn = turns[i];
+ if (!isLocal(turn.id)) {
+ return turn.id;
+ }
+ }
+ return undefined;
+}
diff --git a/src/vs/platform/agentHost/common/agentHostExtensionProtocol.ts b/src/vs/platform/agentHost/common/agentHostExtensionProtocol.ts
index d234b1bf67c..e9a3263ddcc 100644
--- a/src/vs/platform/agentHost/common/agentHostExtensionProtocol.ts
+++ b/src/vs/platform/agentHost/common/agentHostExtensionProtocol.ts
@@ -5,11 +5,31 @@
import { vEnum, vObj, vOptionalProp, vString, type ValidatorType } from '../../../base/common/validation.js';
import type { AgentHostDebugLogsArtifactKind, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult } from './agentService.js';
+import type { InitializeResult } from './state/protocol/common/commands.js';
export const CollectAgentHostDebugLogsExtensionMethod = 'vscode/collectAgentHostDebugLogs';
export const GetAgentHostSessionStateFileExtensionMethod = 'vscode/getAgentHostSessionStateFile';
export const ReadAgentHostDebugLogsChunkExtensionMethod = 'vscode/readAgentHostDebugLogsChunk';
+const AgentHostChatStateFileCapabilityMetaKey = 'vscode.getAgentHostSessionStateFile.chat';
+
+export interface IAgentHostExtensionInitializeResultMeta extends Record {
+ readonly [AgentHostChatStateFileCapabilityMetaKey]?: true;
+}
+
+export interface IAgentHostExtensionInitializeResult extends InitializeResult {
+ readonly _meta?: IAgentHostExtensionInitializeResultMeta;
+}
+
+export function getAgentHostExtensionInitializeResultMeta(): IAgentHostExtensionInitializeResultMeta {
+ return { [AgentHostChatStateFileCapabilityMetaKey]: true };
+}
+
+export function supportsAgentHostChatStateFile(result: IAgentHostExtensionInitializeResult | undefined): boolean {
+ const meta = result?._meta;
+ return meta?.[AgentHostChatStateFileCapabilityMetaKey] === true;
+}
+
export const collectAgentHostDebugLogsParamsValidator = vObj({
session: vOptionalProp(vString()),
chat: vOptionalProp(vString()),
@@ -24,7 +44,7 @@ export interface IAgentHostExtensionCommandMap {
'getManagedSettingsDiagnostics': { params: undefined; result: readonly IAgentHostManagedSettingsDiagnostics[] };
'diagnosticsFetch': { params: { url: string }; result: IAgentHostNetworkFetchResult };
[GetAgentHostSessionStateFileExtensionMethod]: {
- params: { session: string };
+ params: { session: string; chat?: string };
result: { resource?: string };
};
[CollectAgentHostDebugLogsExtensionMethod]: {
diff --git a/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts b/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts
index 39ca9fab306..d96446b3293 100644
--- a/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts
+++ b/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts
@@ -9,7 +9,7 @@ import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { URI } from '../../../base/common/uri.js';
import { createFileSystemProviderError, FileChangeType, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProvider, IFileSystemProviderWithFileRealpathCapability, IFileWriteOptions, IStat, IWatchOptions } from '../../files/common/files.js';
-import { fromAgentHostUri, toAgentHostUri } from './agentHostUri.js';
+import { fromAgentHostUri, isAgentHostContentRefUri, toAgentHostUri } from './agentHostUri.js';
import { ContentEncoding, type CreateResourceWatchParams, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceRequestParams, type ResourceRequestResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWriteParams, type ResourceWriteResult } from './state/protocol/commands.js';
import { AhpErrorCodes } from './state/protocol/errors.js';
import { ProtocolError } from './state/sessionProtocol.js';
@@ -384,16 +384,34 @@ export abstract class AHPFileSystemProvider extends Disposable implements IFileS
return store;
}
+ /**
+ * Whether `resource` addresses a protocol `ContentRef` rather than an entry
+ * in the host's filesystem. See {@link toAgentHostContentUri}.
+ *
+ * The scheme check covers content refs minted before the marker existed,
+ * such as ones persisted in restored editor state. It can go once those
+ * have aged out — and the marker itself can go if `resourceResolve` grows
+ * a way for a host to report a resource as readable but not resolvable.
+ */
+ private _isContentRef(resource: URI, decoded: URI): boolean {
+ return isAgentHostContentRefUri(resource)
+ || decoded.scheme === 'session-db'
+ || decoded.scheme === 'git-blob';
+ }
+
async stat(resource: URI): Promise {
const path = resource.path;
+ // Before the synthetic-root check: a content ref whose original URI has
+ // no path is wrapped as `/`, and it is a file, not the provider root.
+ const decoded = this._decodeUri(resource);
+ if (this._isContentRef(resource, decoded)) {
+ return { type: FileType.File, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
+ }
+
if (path === '/' || path === '') {
return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
}
- const decoded = this._decodeUri(resource);
- if (decoded.scheme === 'session-db' || decoded.scheme === 'git-blob') {
- return { type: FileType.File, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
- }
if (decoded.path === '/' || decoded.path === '') {
return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
@@ -424,7 +442,7 @@ export abstract class AHPFileSystemProvider extends Disposable implements IFileS
return path;
}
const decoded = this._decodeUri(resource);
- if (decoded.scheme === 'session-db' || decoded.scheme === 'git-blob' || decoded.path === '/' || decoded.path === '') {
+ if (this._isContentRef(resource, decoded) || decoded.path === '/' || decoded.path === '') {
return path;
}
const connection = await this._getConnection(resource.authority);
diff --git a/src/vs/platform/agentHost/common/agentHostGitStateService.ts b/src/vs/platform/agentHost/common/agentHostGitStateService.ts
index ee958fe4a12..f8373e1815e 100644
--- a/src/vs/platform/agentHost/common/agentHostGitStateService.ts
+++ b/src/vs/platform/agentHost/common/agentHostGitStateService.ts
@@ -57,7 +57,4 @@ export interface IAgentHostGitStateService {
* @param workingDirectory Optional working directory override; when omitted, the session summary's working directory is used.
*/
attachSessionGitHubPullRequest(sessionKey: string, workingDirectory?: URI): Promise;
-
- /** Adds GitHub issues and pull requests referenced in a user message to the session. */
- attachSessionGitHubReferences(sessionKey: string, text: string): Promise;
}
diff --git a/src/vs/platform/agentHost/common/agentHostPlanReview.ts b/src/vs/platform/agentHost/common/agentHostPlanReview.ts
index 47b29a86b72..79c65859f53 100644
--- a/src/vs/platform/agentHost/common/agentHostPlanReview.ts
+++ b/src/vs/platform/agentHost/common/agentHostPlanReview.ts
@@ -25,3 +25,8 @@ export interface IAgentHostPlanReview {
export type ChatInputRequestWithPlanReview = ChatInputRequest & {
readonly planReview?: IAgentHostPlanReview;
};
+
+export function isChatInputRequestWithPlanReview(request: ChatInputRequest): request is ChatInputRequestWithPlanReview {
+ const candidate: ChatInputRequestWithPlanReview = request;
+ return candidate.planReview !== undefined;
+}
diff --git a/src/vs/platform/agentHost/common/agentHostResourceService.ts b/src/vs/platform/agentHost/common/agentHostResourceService.ts
index 75a48227d31..34dae7ec6bc 100644
--- a/src/vs/platform/agentHost/common/agentHostResourceService.ts
+++ b/src/vs/platform/agentHost/common/agentHostResourceService.ts
@@ -31,9 +31,14 @@ export const enum AgentHostAccessMode {
/**
* Persisted shape of {@link AgentHostLocalFilePermissionsSettingId}:
- * `{ [normalizedAddress]: { [uriString]: 'r' | 'rw' } }`.
+ * `{ [normalizedAddress]: { [canonicalUriString]: 'r' | 'rw' | { mode, lexicalUri } } }`.
*/
-export type AgentHostPermissionsSetting = Record>;
+export type AgentHostPermissionGrant = AgentHostAccessMode | {
+ readonly mode: AgentHostAccessMode;
+ readonly lexicalUri: string;
+};
+
+export type AgentHostPermissionsSetting = Record>;
/**
* Capability a request needs from the user. The protocol-level `read` and
diff --git a/src/vs/platform/agentHost/common/agentHostTelemetry.ts b/src/vs/platform/agentHost/common/agentHostTelemetry.ts
index f404ebebcb9..237376c3386 100644
--- a/src/vs/platform/agentHost/common/agentHostTelemetry.ts
+++ b/src/vs/platform/agentHost/common/agentHostTelemetry.ts
@@ -32,6 +32,13 @@ export const enum AgentHostTransportKind {
Unknown = 'unknown',
}
+/**
+ * The stage a turn reached before it failed. Declared here rather than beside the
+ * telemetry reporter so `common` consumers (such as the chat contribution
+ * admission hook) can name a failure stage without importing from `node`.
+ */
+export type AgentHostTurnFailureStage = 'validation' | 'workingDirectory' | 'modelSelection' | 'sendMessage' | 'provider';
+
export interface IAgentHostClientTelemetryContext {
readonly clientType: AgentHostClientType;
readonly connectionKind: AgentHostClientConnectionKind;
diff --git a/src/vs/platform/agentHost/common/agentHostUri.ts b/src/vs/platform/agentHost/common/agentHostUri.ts
index 55424005a41..a0343f26f18 100644
--- a/src/vs/platform/agentHost/common/agentHostUri.ts
+++ b/src/vs/platform/agentHost/common/agentHostUri.ts
@@ -63,6 +63,12 @@ interface IAgentHostUriMeta {
readonly authority?: string;
/** Original URI query, omitted when empty. */
readonly query?: string;
+ /**
+ * Set when the wrapped URI came from a protocol `ContentRef` rather than
+ * from the host's filesystem. Omitted otherwise. See
+ * {@link toAgentHostContentUri}.
+ */
+ readonly contentRef?: true;
}
/**
@@ -75,6 +81,31 @@ interface IAgentHostUriMeta {
* the URI authority (from {@link agentHostAuthority}).
*/
export function toAgentHostUri(originalUri: URI, connectionAuthority: string): URI {
+ return wrapAgentHostUri(originalUri, connectionAuthority, false);
+}
+
+/**
+ * Wraps a protocol `ContentRef` URI, marking it so the filesystem provider
+ * reads it with `resourceRead` instead of resolving it as a filesystem entry.
+ * Hosts choose their own content URI shapes, so the scheme cannot identify one.
+ *
+ * A content ref that is already a plain `file:` URI on the local connection
+ * stays unwrapped: it addresses a real file and resolves normally.
+ */
+export function toAgentHostContentUri(originalUri: URI, connectionAuthority: string): URI {
+ return wrapAgentHostUri(originalUri, connectionAuthority, true);
+}
+
+/**
+ * Maps a host-side URI into client space.
+ *
+ * `options.contentRef` marks a URI read out of a protocol `ContentRef`, so it
+ * is wrapped with {@link toAgentHostContentUri} rather than
+ * {@link toAgentHostUri}.
+ */
+export type AgentHostUriMapper = (uri: URI, options?: { readonly contentRef?: boolean }) => URI;
+
+function wrapAgentHostUri(originalUri: URI, connectionAuthority: string, contentRef: boolean): URI {
if (connectionAuthority === 'local' && originalUri.scheme === Schemas.file) {
return originalUri;
}
@@ -83,6 +114,7 @@ export function toAgentHostUri(originalUri: URI, connectionAuthority: string): U
scheme: originalUri.scheme,
...(originalUri.authority ? { authority: originalUri.authority } : {}),
...(originalUri.query ? { query: originalUri.query } : {}),
+ ...(contentRef ? { contentRef: true } as const : {}),
};
const params = new URLSearchParams();
params.set(AGENT_HOST_META_PARAM, encodeBase64(VSBuffer.fromString(JSON.stringify(meta)), false, true));
@@ -95,6 +127,35 @@ export function toAgentHostUri(originalUri: URI, connectionAuthority: string): U
});
}
+/**
+ * Reads the {@link IAgentHostUriMeta} payload off a {@link AGENT_HOST_SCHEME}
+ * URI, or `undefined` when it is absent or malformed.
+ */
+function readAgentHostUriMeta(agentHostUri: URI): Partial | undefined {
+ const encoded = agentHostUri.query ? new URLSearchParams(agentHostUri.query).get(AGENT_HOST_META_PARAM) : null;
+ if (!encoded) {
+ return undefined;
+ }
+ try {
+ return JSON.parse(decodeBase64(encoded).toString()) as Partial;
+ } catch {
+ return undefined;
+ }
+}
+
+/**
+ * Whether the URI wraps a protocol `ContentRef` — content read with
+ * `resourceRead`, never resolved with `resourceResolve`.
+ *
+ * See {@link toAgentHostContentUri}.
+ */
+export function isAgentHostContentRefUri(agentHostUri: URI): boolean {
+ if (agentHostUri.scheme !== AGENT_HOST_SCHEME) {
+ return false;
+ }
+ return readAgentHostUriMeta(agentHostUri)?.contentRef === true;
+}
+
/**
* Extracts the original URI from a {@link AGENT_HOST_SCHEME} URI.
*
@@ -105,15 +166,7 @@ export function fromAgentHostUri(agentHostUri: URI): URI {
return agentHostUri;
}
- let meta: Partial | undefined;
- const encoded = agentHostUri.query ? new URLSearchParams(agentHostUri.query).get(AGENT_HOST_META_PARAM) : null;
- if (encoded) {
- try {
- meta = JSON.parse(decodeBase64(encoded).toString()) as Partial;
- } catch {
- meta = undefined;
- }
- }
+ const meta = readAgentHostUriMeta(agentHostUri);
if (!meta || typeof meta.scheme !== 'string') {
// Missing/invalid metadata — fall back to treating the path as a
diff --git a/src/vs/platform/agentHost/common/agentMerge.ts b/src/vs/platform/agentHost/common/agentMerge.ts
index 6470396fbe2..d5ef31e49af 100644
--- a/src/vs/platform/agentHost/common/agentMerge.ts
+++ b/src/vs/platform/agentHost/common/agentMerge.ts
@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../nls.js';
+import { appendEscapedMarkdownInlineCode } from '../../../base/common/htmlContent.js';
+import { structuralEquals } from '../../../base/common/equals.js';
import { createSchema, schemaProperty } from './agentHostSchema.js';
import { GitHubActor, PullRequestCheck, PullRequestChecks, PullRequestSnapshot } from '../../github/common/githubPullRequestService.js';
import { SessionConfigKey } from './sessionConfigKeys.js';
@@ -198,6 +200,77 @@ export function resolveAgentMergeConfiguration(defaults: AgentMergeConfiguration
};
}
+/**
+ * Why Agent Merge stopped monitoring a session. Keeping both strings together
+ * lets the controller log a stable English detail while the transcript shows a
+ * localized sentence, without either drifting from the other.
+ */
+export interface AgentMergeDisableReason {
+ /** Stable English detail appended to the host log line. */
+ readonly log: string;
+ /** Localized sentence shown to the user in the session transcript. */
+ readonly notice: string;
+}
+
+/** Every reason the Agent Merge controller can stop monitoring a session on its own. */
+export const agentMergeDisableReasons = {
+ sessionArchived: (): AgentMergeDisableReason => ({
+ log: 'the session was archived',
+ notice: localize('agentMerge.disabled.sessionArchived', "Agent Merge was turned off because this session was archived."),
+ }),
+ branchChanged: (from: string, to: string): AgentMergeDisableReason => ({
+ log: `branch changed from ${from} to ${to}`,
+ notice: localize(
+ 'agentMerge.disabled.branchChanged',
+ "Agent Merge was turned off because the checked-out branch changed from {0} to {1}.",
+ appendEscapedMarkdownInlineCode(from),
+ appendEscapedMarkdownInlineCode(to)
+ ),
+ }),
+ branchChangedWhileRefreshing: (): AgentMergeDisableReason => ({
+ log: 'the checked-out branch changed while pull request state was refreshing',
+ notice: localize('agentMerge.disabled.branchChangedWhileRefreshing', "Agent Merge was turned off because the checked-out branch changed while its pull request state was refreshing."),
+ }),
+ differentPullRequest: (): AgentMergeDisableReason => ({
+ log: 'the session became associated with a different pull request',
+ notice: localize('agentMerge.disabled.differentPullRequest', "Agent Merge was turned off because this session became associated with a different pull request."),
+ }),
+ invalidPullRequestUrl: (): AgentMergeDisableReason => ({
+ log: 'the associated pull request URL is invalid',
+ notice: localize('agentMerge.disabled.invalidPullRequestUrl', "Agent Merge was turned off because the associated pull request URL is invalid."),
+ }),
+ differentGitHubHost: (): AgentMergeDisableReason => ({
+ log: 'the bound pull request belongs to a different GitHub host than the signed-in account',
+ notice: localize('agentMerge.disabled.differentGitHubHost', "Agent Merge was turned off because its pull request belongs to a different GitHub host than the signed-in account."),
+ }),
+ indeterminate: (minutes: number, reason: string): AgentMergeDisableReason => ({
+ log: `the pull request state could not be evaluated for ${minutes} minutes: ${reason}`,
+ notice: localize('agentMerge.disabled.indeterminate', "Agent Merge was turned off because its pull request state could not be evaluated for {0} minutes.", minutes),
+ }),
+ pullRequestClosed: (): AgentMergeDisableReason => ({
+ log: 'the pull request is closed or merged',
+ notice: localize('agentMerge.disabled.pullRequestClosed', "Agent Merge was turned off because its pull request is closed or merged."),
+ }),
+ repairBudgetExhausted: (): AgentMergeDisableReason => ({
+ log: 'the same pull request blockers remained after repeated repair attempts',
+ notice: localize('agentMerge.disabled.repairBudgetExhausted', "Agent Merge was turned off because the same pull request blockers remained after repeated repair attempts."),
+ }),
+ pullRequestMerged: (): AgentMergeDisableReason => ({
+ log: 'the pull request was merged',
+ notice: localize('agentMerge.disabled.pullRequestMerged', "Agent Merge merged its pull request and turned itself off."),
+ }),
+} as const;
+
+/** The transcript notice shown once Agent Merge starts watching a branch. */
+export function agentMergeEnabledNotice(branchName: string): string {
+ return localize('agentMerge.notice.enabled', "Agent Merge is on and watching {0}.", appendEscapedMarkdownInlineCode(branchName));
+}
+
+/** The transcript notice shown when the user, rather than the controller, turns Agent Merge off. */
+export function agentMergeDisabledNotice(): string {
+ return localize('agentMerge.notice.disabled', "Agent Merge was turned off for this session.");
+}
+
export function readAgentMergeSessionState(values: Record | undefined): AgentMergeSessionState | undefined {
const value = values?.[SessionConfigKey.AgentMerge];
if (!isRecord(value) || typeof value.enabled !== 'boolean') {
@@ -219,6 +292,38 @@ export function readAgentMergeSessionState(values: Record | und
};
}
+/**
+ * Returns session config values with Agent Merge injected overrides removed,
+ * so callers can read the user's own picker selections while merge is active.
+ */
+export function getNonMergeSessionConfigValues(values: Readonly> | undefined): Readonly> {
+ if (!values) {
+ return {};
+ }
+ const agentMerge = readAgentMergeSessionState(values as Record);
+ const injected = agentMerge?.injectedConfiguration;
+ if (!agentMerge?.enabled || !injected) {
+ return values;
+ }
+ const restored = { ...values };
+ for (const [key, appliedValue] of Object.entries(injected.applied)) {
+ if (!structuralEquals(restored[key], appliedValue)) {
+ continue;
+ }
+ if (Object.hasOwn(injected.previous, key)) {
+ const previousValue = injected.previous[key];
+ if (previousValue === undefined) {
+ delete restored[key];
+ } else {
+ restored[key] = previousValue;
+ }
+ } else {
+ delete restored[key];
+ }
+ }
+ return restored;
+}
+
export function isAgentMergeFeedbackAuthor(actor: GitHubActor | undefined): boolean {
if (!actor) {
return false;
diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts
index 1b1c41cc5c6..628ea750bdd 100644
--- a/src/vs/platform/agentHost/common/agentService.ts
+++ b/src/vs/platform/agentHost/common/agentService.ts
@@ -297,13 +297,14 @@ export function getAgentHostCopilotSandboxSettingId(customTerminalToolEnabled: b
export const CodexPreferAgentHostEditorSettingId = 'chat.editor.codex.preferAgentHost';
export function affectsAgentHostProviderPreference(event: IConfigurationChangeEvent, isSessionsWindow: boolean): boolean {
- return event.affectsConfiguration(isSessionsWindow ? AgentHostCodexAgentEnabledSettingId : CodexPreferAgentHostEditorSettingId);
+ return event.affectsConfiguration(AgentHostClaudeAgentEnabledSettingId)
+ || event.affectsConfiguration(isSessionsWindow ? AgentHostCodexAgentEnabledSettingId : CodexPreferAgentHostEditorSettingId);
}
export function shouldSurfaceLocalAgentHostProvider(provider: AgentProvider, configurationService: IConfigurationService, isSessionsWindow: boolean): boolean {
switch (provider) {
case CLAUDE_AGENT_PROVIDER_ID:
- return true;
+ return configurationService.getValue(AgentHostClaudeAgentEnabledSettingId) !== false;
case CODEX_AGENT_PROVIDER_ID:
return configurationService.getValue(isSessionsWindow ? AgentHostCodexAgentEnabledSettingId : CodexPreferAgentHostEditorSettingId) === true;
default:
@@ -777,7 +778,7 @@ export interface IAgentHostManagementService {
getNetworkDiagnosticsInfo(): Promise;
getManagedSettingsDiagnostics(): Promise;
diagnosticsFetch(url: string): Promise;
- getSessionStateFile(session: URI): Promise;
+ getSessionStateFile(session: URI, chat?: URI): Promise;
collectDebugLogs(session: URI | undefined, kind: AgentHostDebugLogsArtifactKind, chat?: URI): Promise;
readDebugLogsChunk(resource: URI, position: number): Promise;
startWebSocketServer(): Promise;
@@ -909,7 +910,7 @@ export interface IAgentService {
*/
diagnosticsFetch(url: string): Promise;
- getSessionStateFile?(session: URI): Promise;
+ getSessionStateFile?(session: URI, chat?: URI): Promise;
collectDebugLogs?(session: URI | undefined, kind: AgentHostDebugLogsArtifactKind, chat?: URI): Promise;
@@ -923,9 +924,11 @@ export interface IAgentService {
* resource arrive via {@link onDidAction}. Registers `clientId` against
* the resource so the server-side refcount knows who is watching, so the
* caller does not need to invoke {@link addSubscriber} separately. Pair
- * with {@link unsubscribe} when the subscription is released.
+ * with {@link unsubscribe} when the subscription is released. When
+ * provided, `isActive` is checked before registering the subscriber so a
+ * request cancelled during asynchronous resolution cannot pin the resource.
*/
- subscribe(resource: URI, clientId: string): Promise;
+ subscribe(resource: URI, clientId: string, isActive?: () => boolean): Promise;
/**
* Counterpart to {@link subscribe}. Drops `clientId` from the refcount
@@ -1143,7 +1146,7 @@ export interface IAgentConnection {
*/
diagnosticsFetch(url: string): Promise;
- getSessionStateFile(session: URI): Promise;
+ getSessionStateFile(session: URI, chat?: URI): Promise;
collectDebugLogs(session: URI | undefined, kind: AgentHostDebugLogsArtifactKind, chat?: URI): Promise;
diff --git a/src/vs/platform/agentHost/common/changesetUri.ts b/src/vs/platform/agentHost/common/changesetUri.ts
index db6c6cb99d8..60f424a76df 100644
--- a/src/vs/platform/agentHost/common/changesetUri.ts
+++ b/src/vs/platform/agentHost/common/changesetUri.ts
@@ -117,6 +117,24 @@ export const enum ChangesetKind {
Unknown = 'unknown',
}
+/** RFC 3986 scheme prefix, e.g. the `ahp-session:` in `ahp-session:/abc`. */
+const URI_SCHEME_PREFIX = /^[a-zA-Z][a-zA-Z0-9+.\-]*:/;
+
+/**
+ * Resolve a {@link Changeset.uriTemplate} from a session's catalogue into a
+ * subscribable URI template.
+ *
+ * A host may publish the template relative to the session channel
+ * (`changeset/branch`); used verbatim that addresses the client's own
+ * filesystem. Templates that already carry a scheme are returned unchanged.
+ */
+export function resolveChangesetUriTemplate(sessionUri: URI, uriTemplate: string): string {
+ if (URI_SCHEME_PREFIX.test(uriTemplate)) {
+ return uriTemplate;
+ }
+ return `${sessionUri.replace(/\/+$/, '')}/${uriTemplate.replace(/^\/+/, '')}`;
+}
+
export function buildBranchChangesetUri(sessionUri: URI): URI {
return `${sessionUri}${CHANGESET_PATH_SEGMENT}${BRANCH_CHANGESET_ID}`;
}
diff --git a/src/vs/platform/agentHost/common/githubIssueReferences.ts b/src/vs/platform/agentHost/common/githubIssueReferences.ts
index c2980802738..958862a3840 100644
--- a/src/vs/platform/agentHost/common/githubIssueReferences.ts
+++ b/src/vs/platform/agentHost/common/githubIssueReferences.ts
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-/** A GitHub issue referenced from a user message. */
+/** A GitHub issue, identified by the repository that owns it and its number. */
export interface IGitHubIssueReference {
readonly owner: string;
readonly repo: string;
@@ -11,64 +11,19 @@ export interface IGitHubIssueReference {
}
/**
- * Matches `https://github.com/{owner}/{repo}/issues/{number}`, optionally with a
- * `www.` host, a trailing slash, a query string or a fragment (e.g. the
- * `#issuecomment-123` anchor GitHub appends when copying a comment link).
+ * Matches `https://github.com/{owner}/{repo}/issues/{number}` from the start of
+ * the string, optionally with a `www.` host. The trailing boundary lets a URL
+ * keep a trailing slash, query string or fragment (e.g. the `#issuecomment-123`
+ * anchor GitHub appends when copying a comment link).
*/
-const ISSUE_URL_PATTERN = /\bhttps?:\/\/(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/issues\/(\d+)\b/gi;
+const ISSUE_URL_PATTERN = /^https?:\/\/(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/issues\/(\d+)\b/i;
-/**
- * Matches the cross-repository shorthand `{owner}/{repo}#{number}`. The leading
- * boundary check rejects references that are part of a longer path (e.g. the
- * `microsoft/vscode#1` inside a URL, which the URL pattern already covers).
- */
-const ISSUE_SHORTHAND_PATTERN = /(?();
-
- const add = (owner: string, repo: string, rawNumber: string): void => {
- const number = Number(rawNumber);
- if (!Number.isSafeInteger(number) || number <= 0) {
- return;
- }
- const url = toGitHubIssueUrl({ owner, repo, number });
- if (seen.has(url)) {
- return;
- }
- seen.add(url);
- references.push({ owner, repo, number });
- };
-
- for (const match of text.matchAll(ISSUE_URL_PATTERN)) {
- add(match[1], match[2], match[3]);
- }
- for (const match of text.matchAll(ISSUE_SHORTHAND_PATTERN)) {
- add(match[1], match[2], match[3]);
- }
-
- return references;
-}
-
-/** Builds the canonical `github.com` URL for an issue reference. */
-export function toGitHubIssueUrl(reference: IGitHubIssueReference): string {
- return `https://github.com/${reference.owner}/${reference.repo}/issues/${reference.number}`;
-}
-
-/** Parses a canonical GitHub issue URL back into its parts, or `undefined`. */
+/** Parses a GitHub issue URL into its parts, or `undefined` when it is not one. */
export function parseGitHubIssueUrl(url: string): IGitHubIssueReference | undefined {
- return parseGitHubIssueReferences(url)[0];
+ const match = ISSUE_URL_PATTERN.exec(url);
+ if (!match) {
+ return undefined;
+ }
+ const number = Number(match[3]);
+ return Number.isSafeInteger(number) && number > 0 ? { owner: match[1], repo: match[2], number } : undefined;
}
diff --git a/src/vs/platform/agentHost/common/githubPullRequestReferences.ts b/src/vs/platform/agentHost/common/githubPullRequestReferences.ts
deleted file mode 100644
index 8a3a698e5d3..00000000000
--- a/src/vs/platform/agentHost/common/githubPullRequestReferences.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-/** A GitHub pull request referenced from a user message. */
-export interface IGitHubPullRequestReference {
- readonly owner: string;
- readonly repo: string;
- readonly number: number;
-}
-
-const PULL_REQUEST_URL_PATTERN = /\bhttps?:\/\/(?[\w.-]+)\/(?[\w.-]+)\/(?[\w.-]+)\/pull\/(?\d+)\b/gi;
-const PULL_REQUEST_SHORTHAND_PATTERN = /\b(?:PR|pull request)\s*#(?\d+)\b/gi;
-
-/** Extracts unambiguous GitHub pull request references without duplicates. */
-export function parseGitHubPullRequestReferences(text: string, defaultRepository?: { readonly owner: string; readonly repo: string }, gitHubHost = 'github.com'): IGitHubPullRequestReference[] {
- const candidates: (IGitHubPullRequestReference & { readonly index: number })[] = [];
- const references: IGitHubPullRequestReference[] = [];
- const seen = new Set();
- const normalizedGitHubHost = normalizeGitHubHost(gitHubHost);
-
- const addCandidate = (index: number, owner: string, repo: string, rawNumber: string): void => {
- const number = Number(rawNumber);
- if (!Number.isSafeInteger(number) || number <= 0) {
- return;
- }
- candidates.push({ index, owner, repo, number });
- };
-
- for (const match of text.matchAll(PULL_REQUEST_URL_PATTERN)) {
- if (match.groups && normalizeGitHubHost(match.groups.host) === normalizedGitHubHost) {
- addCandidate(match.index, match.groups.owner, match.groups.repo, match.groups.number);
- }
- }
- if (defaultRepository) {
- for (const match of text.matchAll(PULL_REQUEST_SHORTHAND_PATTERN)) {
- if (match.groups) {
- addCandidate(match.index, defaultRepository.owner, defaultRepository.repo, match.groups.number);
- }
- }
- }
-
- for (const candidate of candidates.sort((a, b) => a.index - b.index)) {
- const { owner, repo, number } = candidate;
- const reference = { owner, repo, number };
- const url = toGitHubPullRequestUrl(reference, gitHubHost).toLowerCase();
- if (!seen.has(url)) {
- seen.add(url);
- references.push(reference);
- }
- }
-
- return references;
-}
-
-function normalizeGitHubHost(host: string): string {
- return host.toLowerCase().replace(/^www\./, '');
-}
-
-/** Builds the canonical URL for a pull request reference on the configured GitHub host. */
-export function toGitHubPullRequestUrl(reference: IGitHubPullRequestReference, gitHubHost = 'github.com'): string {
- return `https://${normalizeGitHubHost(gitHubHost)}/${reference.owner}/${reference.repo}/pull/${reference.number}`;
-}
diff --git a/src/vs/platform/agentHost/common/meta/agentMergeMessageMeta.ts b/src/vs/platform/agentHost/common/meta/agentMergeMessageMeta.ts
new file mode 100644
index 00000000000..0426d2e5717
--- /dev/null
+++ b/src/vs/platform/agentHost/common/meta/agentMergeMessageMeta.ts
@@ -0,0 +1,28 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+const AGENT_MERGE_MESSAGE_META_KEY = 'vscode.chat.agentMerge';
+
+interface IHasAgentMergeMessageMeta {
+ readonly _meta?: Record;
+}
+
+/**
+ * Whether Agent Merge produced the message, i.e. whether the turn it starts is
+ * an automated repair run rather than one a person or an agent asked for.
+ *
+ * Agent Merge prompts carry the protocol's `systemNotification` origin, which
+ * they share with every other host-generated message, so this marker is what
+ * tells them apart.
+ */
+export function isAgentMergeMessage(source: IHasAgentMergeMessageMeta): boolean {
+ // eslint-disable-next-line local/code-no-untyped-meta-access -- sanctioned first hop into the namespaced Agent Merge slot; validated here.
+ return source._meta?.[AGENT_MERGE_MESSAGE_META_KEY] === true;
+}
+
+/** Serializes the Agent Merge message marker for the open protocol bag. */
+export function toAgentMergeMessageMeta(): Record {
+ return { [AGENT_MERGE_MESSAGE_META_KEY]: true };
+}
diff --git a/src/vs/platform/agentHost/common/meta/agentMessageDelegationMeta.ts b/src/vs/platform/agentHost/common/meta/agentMessageDelegationMeta.ts
index d1b18d16bae..7be08f633d0 100644
--- a/src/vs/platform/agentHost/common/meta/agentMessageDelegationMeta.ts
+++ b/src/vs/platform/agentHost/common/meta/agentMessageDelegationMeta.ts
@@ -9,22 +9,46 @@ interface IHasMessageDelegationMeta {
readonly _meta?: Record;
}
-export interface IAgentMessageDelegationMeta {
+export interface IAgentMessageThreadDelegationMeta {
readonly sourceThreadId: string;
}
+export interface IAgentMessageSessionDelegationMeta {
+ readonly sourceSession: string;
+ readonly sourceChat?: string;
+ readonly sourceTurnId?: string;
+}
+
+export type IAgentMessageDelegationMeta = IAgentMessageThreadDelegationMeta | IAgentMessageSessionDelegationMeta;
+
+/** Parses recognized Agent Host message-delegation metadata. */
+export function parseAgentMessageDelegationMeta(value: unknown): IAgentMessageDelegationMeta | undefined {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
+ return undefined;
+ }
+ const candidate = value as Record;
+ const sourceThreadId = candidate['sourceThreadId'];
+ if (typeof sourceThreadId === 'string' && sourceThreadId.length > 0) {
+ return { sourceThreadId };
+ }
+ const sourceSession = candidate['sourceSession'];
+ if (typeof sourceSession !== 'string' || sourceSession.length === 0) {
+ return undefined;
+ }
+ return {
+ sourceSession,
+ ...(typeof candidate['sourceChat'] === 'string' ? { sourceChat: candidate['sourceChat'] } : {}),
+ ...(typeof candidate['sourceTurnId'] === 'string' ? { sourceTurnId: candidate['sourceTurnId'] } : {}),
+ };
+}
+
/** Reads recognized Agent Host message-delegation metadata. */
export function readAgentMessageDelegationMeta(source: IHasMessageDelegationMeta): IAgentMessageDelegationMeta | undefined {
// eslint-disable-next-line local/code-no-untyped-meta-access -- sanctioned first hop into the namespaced delegation slot; validated below.
- const value = source._meta?.[MESSAGE_DELEGATION_META_KEY];
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
- return undefined;
- }
- const sourceThreadId = (value as Record)['sourceThreadId'];
- return typeof sourceThreadId === 'string' && sourceThreadId.length > 0 ? { sourceThreadId } : undefined;
+ return parseAgentMessageDelegationMeta(source._meta?.[MESSAGE_DELEGATION_META_KEY]);
}
/** Serializes Agent Host message-delegation metadata for the open protocol bag. */
export function toAgentMessageDelegationMeta(meta: IAgentMessageDelegationMeta): Record {
- return { [MESSAGE_DELEGATION_META_KEY]: { sourceThreadId: meta.sourceThreadId } };
+ return { [MESSAGE_DELEGATION_META_KEY]: meta };
}
diff --git a/src/vs/platform/agentHost/common/meta/agentSystemNotificationMeta.ts b/src/vs/platform/agentHost/common/meta/agentSystemNotificationMeta.ts
index e9942be7c08..1f795310e9a 100644
--- a/src/vs/platform/agentHost/common/meta/agentSystemNotificationMeta.ts
+++ b/src/vs/platform/agentHost/common/meta/agentSystemNotificationMeta.ts
@@ -5,12 +5,22 @@
export const enum AgentSystemNotificationKind {
WorktreeCreationFailure = 'worktreeCreationFailure',
+ /** Agent Merge started monitoring the session's branch. */
+ AgentMergeEnabled = 'agentMergeEnabled',
+ /** Agent Merge stopped monitoring the session, usually on its own. */
+ AgentMergeDisabled = 'agentMergeDisabled',
}
export const enum AgentSystemNotificationSeverity {
Warning = 'warning',
}
+const knownKinds: ReadonlySet = new Set([
+ AgentSystemNotificationKind.WorktreeCreationFailure,
+ AgentSystemNotificationKind.AgentMergeEnabled,
+ AgentSystemNotificationKind.AgentMergeDisabled,
+]);
+
interface IHasSystemNotificationMeta {
readonly _meta?: Record;
}
@@ -26,8 +36,9 @@ export function readAgentSystemNotificationMeta(source: IHasSystemNotificationMe
if (!meta) {
return {};
}
+ const kind = meta['kind'];
return {
- kind: meta['kind'] === AgentSystemNotificationKind.WorktreeCreationFailure ? meta['kind'] : undefined,
+ kind: typeof kind === 'string' && knownKinds.has(kind) ? kind as AgentSystemNotificationKind : undefined,
severity: meta['severity'] === AgentSystemNotificationSeverity.Warning ? meta['severity'] : undefined,
};
}
diff --git a/src/vs/platform/agentHost/common/openSessionLink.ts b/src/vs/platform/agentHost/common/openSessionLink.ts
index f806de68711..449d08457e3 100644
--- a/src/vs/platform/agentHost/common/openSessionLink.ts
+++ b/src/vs/platform/agentHost/common/openSessionLink.ts
@@ -22,10 +22,12 @@ import { DEFAULT_CHAT_ID, isAhpChatChannel, parseChatUri } from './state/session
*/
export const AGENT_HOST_SESSION_LINK_SCHEME = 'agent-host-session';
export const AGENT_HOST_SESSION_LINK_PATTERN = /^agent-host-session:\/\/[^/?#]+\/[^?#]+(?:\?[^#]*)?(?:#.*)?$/i;
+export const AGENT_HOST_SESSION_ONLY_LINK_PATTERN = /^(?![^#]*[?&]chat=)agent-host-session:\/\/[^/?#]+\/[^?#]+(?:\?[^#]*)?(?:#.*)?$/i;
+export const AGENT_HOST_CHAT_LINK_PATTERN = /^(?=[^#]*[?&]chat=)agent-host-session:\/\/[^/?#]+\/[^?#]+(?:\?[^#]*)?(?:#.*)?$/i;
export type AgentSessionLinkStatus = 'untitled' | 'inProgress' | 'needsInput' | 'completed' | 'error';
-export function createAgentSessionLinkPresentation(title: string, description: string | undefined, status: AgentSessionLinkStatus, kind: 'session' | 'chat' = 'session'): ILinkPresentation {
+export function buildAgentSessionLinkPresentation(title: string, description: string | undefined, status: AgentSessionLinkStatus, kind: 'session' | 'chat' = 'session'): ILinkPresentation {
const presentationStatus = getAgentSessionLinkPresentationStatus(status);
return {
kind,
@@ -84,14 +86,21 @@ export function isSendMessageTool(toolName: string): boolean {
* ecosystem-wide invariant (an absent chat id already means "the default chat"),
* so it is enforced here once rather than at each call site.
*/
-export function buildOpenSessionLinkUri(backendSession: URI | string, chatId?: string): string {
+export function buildOpenSessionLinkUri(backendSession: URI | string, chatId?: string, turnId?: string): string {
const provider = AgentSession.provider(backendSession);
const rawId = AgentSession.id(backendSession);
if (!provider) {
throw new Error(`Cannot build open-session link: missing provider in ${backendSession.toString()}`);
}
const base = URI.from({ scheme: AGENT_HOST_SESSION_LINK_SCHEME, authority: provider, path: `/${rawId}` }).toString();
- return chatId && chatId !== DEFAULT_CHAT_ID ? `${base}?chat=${encodeURIComponent(chatId)}` : base;
+ const query: string[] = [];
+ if (chatId && chatId !== DEFAULT_CHAT_ID) {
+ query.push(`chat=${encodeURIComponent(chatId)}`);
+ }
+ if (turnId) {
+ query.push(`turn=${encodeURIComponent(turnId)}`);
+ }
+ return query.length > 0 ? `${base}?${query.join('&')}` : base;
}
/**
@@ -121,18 +130,25 @@ export function parseOpenSessionLinkUri(uri: URI | string): URI | undefined {
* links resolving to the default chat.
*/
export function parseOpenSessionLinkChatId(uri: URI | string): string | undefined {
+ const chatId = readOpenSessionLinkQueryParam(uri, 'chat');
+ return chatId === DEFAULT_CHAT_ID ? undefined : chatId;
+}
+
+export function parseOpenSessionLinkTurnId(uri: URI | string): string | undefined {
+ return readOpenSessionLinkQueryParam(uri, 'turn');
+}
+
+function readOpenSessionLinkQueryParam(uri: URI | string, name: string): string | undefined {
const parsed = typeof uri === 'string' ? URI.parse(uri) : uri;
if (parsed.scheme !== AGENT_HOST_SESSION_LINK_SCHEME) {
return undefined;
}
- const match = /(?:^|&)chat=([^&]+)/.exec(parsed.query);
- let chatId: string | undefined;
+ const match = new RegExp(`(?:^|&)${name}=([^&]+)`).exec(parsed.query);
try {
- chatId = match ? decodeURIComponent(match[1]) : undefined;
+ return match ? decodeURIComponent(match[1]) : undefined;
} catch {
return undefined;
}
- return chatId === DEFAULT_CHAT_ID ? undefined : chatId;
}
/**
diff --git a/src/vs/platform/agentHost/common/serverToolNames.ts b/src/vs/platform/agentHost/common/serverToolNames.ts
index beea8e6ecec..9d0113ae1b1 100644
--- a/src/vs/platform/agentHost/common/serverToolNames.ts
+++ b/src/vs/platform/agentHost/common/serverToolNames.ts
@@ -29,7 +29,18 @@ export const enum SessionServerToolName {
/** Names of the artifact server tools, shared between `common/` and `node/`. */
export const enum ArtifactServerToolName {
- AddArtifact = 'add_artifact',
- RemoveArtifact = 'remove_artifact',
- ListArtifacts = 'list_artifacts',
+ AddArtifactOrReference = 'add_artifact_or_reference',
+ RemoveArtifactOrReference = 'remove_artifact_or_reference',
+ ListArtifactsAndReferences = 'list_artifacts_and_references',
}
+
+/**
+ * The names these tools were advertised under before they also recorded
+ * references, mapped to their replacement. Restored history and prompts written
+ * against the old names keep routing and keep their display.
+ */
+export const LEGACY_ARTIFACT_SERVER_TOOL_NAMES: ReadonlyMap = new Map([
+ ['add_artifact', ArtifactServerToolName.AddArtifactOrReference as string],
+ ['remove_artifact', ArtifactServerToolName.RemoveArtifactOrReference as string],
+ ['list_artifacts', ArtifactServerToolName.ListArtifactsAndReferences as string],
+]);
diff --git a/src/vs/platform/agentHost/common/sessionArtifactCollection.ts b/src/vs/platform/agentHost/common/sessionArtifactCollection.ts
index 7cdef0d402e..cfee8db9b5e 100644
--- a/src/vs/platform/agentHost/common/sessionArtifactCollection.ts
+++ b/src/vs/platform/agentHost/common/sessionArtifactCollection.ts
@@ -3,22 +3,24 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
+import { URI } from '../../../base/common/uri.js';
import { getSessionArtifactValue, isGitHubArtifactLink, SESSION_ARTIFACT_TYPES, SessionArtifactType, type ISessionArtifact } from './sessionArtifacts.js';
-/** The fields an agent supplies when adding an artifact. */
+/** The fields an agent supplies when adding an artifact or reference. */
export interface ISessionArtifactInput {
readonly type: SessionArtifactType;
readonly label: string;
+ /** `true` for an artifact the session produced, `false` for a reference. */
+ readonly isArtifact: boolean;
readonly link?: string;
readonly uri?: string;
readonly commitHash?: string;
- readonly createdByThisSession?: boolean;
}
export interface IAddSessionArtifactResult {
readonly artifacts: readonly ISessionArtifact[];
readonly artifact: ISessionArtifact;
- /** `false` when an artifact with the same value already existed. */
+ /** `false` when an entry with the same value already existed. */
readonly added: boolean;
}
@@ -57,7 +59,28 @@ function requireWebLink(value: unknown, field: string, toolName: string): string
return link;
}
-/** Validates and normalizes raw `add_artifact` arguments. */
+/**
+ * The client opens a `uri` by parsing it strictly, so anything it cannot parse
+ * would be recorded, reported as added, and then quietly appear in no pill at
+ * all. Parsing it the same way here gives the agent an error it can act on
+ * instead. A single-letter scheme is a Windows drive path (`C:\repo\plan.md`),
+ * which parses into a nonsense URI rather than failing, so it is rejected too.
+ */
+function requireUri(value: unknown, field: string, toolName: string): string {
+ const uri = requireString(value, field, toolName);
+ let scheme: string | undefined;
+ try {
+ scheme = URI.parse(uri, /*strict*/ true).scheme;
+ } catch {
+ scheme = undefined;
+ }
+ if (!scheme || scheme.length === 1) {
+ throw new Error(`Invalid ${toolName} input: ${field} must be an absolute URI including its scheme, such as 'file:///path/to/file' — not a plain file system path.`);
+ }
+ return uri;
+}
+
+/** Validates and normalizes raw `add_artifact_or_reference` arguments. */
export function parseSessionArtifactInput(rawArgs: unknown, toolName: string): ISessionArtifactInput {
if (!rawArgs || typeof rawArgs !== 'object' || Array.isArray(rawArgs)) {
throw new Error(`Invalid ${toolName} input: expected an object.`);
@@ -67,34 +90,33 @@ export function parseSessionArtifactInput(rawArgs: unknown, toolName: string): I
if (typeof type !== 'string' || !(SESSION_ARTIFACT_TYPES as readonly string[]).includes(type)) {
throw new Error(`Invalid ${toolName} input: type must be one of ${SESSION_ARTIFACT_TYPES.join(', ')}.`);
}
+ if (typeof args['isArtifact'] !== 'boolean') {
+ throw new Error(`Invalid ${toolName} input: isArtifact must be a boolean — true for something this session produced, false for a reference.`);
+ }
const artifactType = type as SessionArtifactType;
- const input: { type: SessionArtifactType; label: string; link?: string; uri?: string; commitHash?: string; createdByThisSession?: boolean } = {
+ const input: { type: SessionArtifactType; label: string; isArtifact: boolean; link?: string; uri?: string; commitHash?: string } = {
type: artifactType,
label: requireString(args['label'], 'label', toolName),
+ isArtifact: args['isArtifact'],
};
if (linkTypes.has(artifactType)) {
input.link = requireWebLink(args['link'], 'link', toolName);
}
if (uriTypes.has(artifactType)) {
- input.uri = requireString(args['uri'], 'uri', toolName);
+ input.uri = requireUri(args['uri'], 'uri', toolName);
}
if (artifactType === SessionArtifactType.Commit) {
input.commitHash = requireString(args['commitHash'], 'commitHash', toolName);
}
- if (artifactType === SessionArtifactType.PullRequest) {
- if (typeof args['createdByThisSession'] !== 'boolean') {
- throw new Error(`Invalid ${toolName} input: createdByThisSession must be a boolean for pull request artifacts.`);
- }
- input.createdByThisSession = args['createdByThisSession'];
- }
return input;
}
/**
- * The artifacts recorded on a session. Immutable: mutations return the next
- * list so callers stay in control of persisting and publishing it.
+ * The artifacts and references recorded on a session. Immutable: mutations
+ * return the next list so callers stay in control of persisting and publishing
+ * it.
*/
export class SessionArtifactCollection {
@@ -105,8 +127,8 @@ export class SessionArtifactCollection {
}
/**
- * Adds an artifact unless one with the same value already exists, in which
- * case the existing artifact is returned unchanged.
+ * Adds an artifact or reference unless one with the same value already
+ * exists, in which case the existing entry is returned unchanged.
*/
add(input: ISessionArtifactInput, createId: () => string): IAddSessionArtifactResult {
const artifact = this._create(input, createId);
@@ -131,18 +153,17 @@ export class SessionArtifactCollection {
id: string;
type: SessionArtifactType;
label: string;
+ isArtifact: boolean;
link?: string;
uri?: string;
commitHash?: string;
isGitHub?: boolean;
- createdByThisSession?: boolean;
- } = { id: createId(), type: input.type, label: input.label };
+ } = { id: createId(), type: input.type, label: input.label, isArtifact: input.isArtifact };
if (input.link !== undefined) { artifact.link = input.link; }
if (input.uri !== undefined) { artifact.uri = input.uri; }
if (input.commitHash !== undefined) { artifact.commitHash = input.commitHash; }
if (input.link !== undefined && gitHubTypes.has(input.type)) { artifact.isGitHub = isGitHubArtifactLink(input.link); }
- if (input.createdByThisSession !== undefined) { artifact.createdByThisSession = input.createdByThisSession; }
return artifact;
}
}
diff --git a/src/vs/platform/agentHost/common/sessionArtifacts.ts b/src/vs/platform/agentHost/common/sessionArtifacts.ts
index 41e0d7ca599..58cd7fc851d 100644
--- a/src/vs/platform/agentHost/common/sessionArtifacts.ts
+++ b/src/vs/platform/agentHost/common/sessionArtifacts.ts
@@ -6,8 +6,10 @@
import type { SessionSummaryMeta } from './state/sessionState.js';
/**
- * Artifact kinds an agent can record on its session. Each kind carries the one
- * field the client needs to open it, plus a label.
+ * The kinds an agent can record on its session, as either an artifact (the
+ * session produced it) or a reference (the session found it worth returning
+ * to). Each kind carries the one field the client needs to open it, plus a
+ * label.
*/
export const enum SessionArtifactType {
PullRequest = 'pullRequest',
@@ -27,26 +29,30 @@ export const SESSION_ARTIFACT_TYPES: readonly SessionArtifactType[] = [
SessionArtifactType.Resource,
];
-/** A session artifact as stored by the host and published to clients. */
+/** A session artifact or reference as stored by the host and published to clients. */
export interface ISessionArtifact {
readonly id: string;
readonly type: SessionArtifactType;
readonly label: string;
- /** Link for pull request, issue, commit and website artifacts. */
+ /**
+ * `true` for an artifact — something this session produced — and `false` for
+ * a reference, something it only points the user at.
+ */
+ readonly isArtifact: boolean;
+ /** Link for pull request, issue, commit and website entries. */
readonly link?: string;
- /** Resource URI for file and resource artifacts. */
+ /** Resource URI for file and resource entries. */
readonly uri?: string;
- /** Commit hash for commit artifacts. */
+ /** Commit hash for commit entries. */
readonly commitHash?: string;
/** Whether a pull request or issue link points at GitHub. Host-computed. */
readonly isGitHub?: boolean;
- /** Whether this session created the pull request, rather than only referencing it. */
- readonly createdByThisSession?: boolean;
}
/**
* Reserved key under {@link SessionSummaryMeta} holding the session's agent-set
- * artifacts. VS Code convention layered on the protocol's generic `_meta` bag.
+ * artifacts and references. VS Code convention layered on the protocol's
+ * generic `_meta` bag.
*/
export const SESSION_META_ARTIFACTS_KEY = 'agentHost/sessionArtifacts';
@@ -62,22 +68,33 @@ function parseSessionArtifact(value: unknown): ISessionArtifact | undefined {
if (typeof raw['id'] !== 'string' || typeof raw['label'] !== 'string' || !isSessionArtifactType(raw['type'])) {
return undefined;
}
+ // `isArtifact` is mandatory, so only its absence is tolerated — that is an
+ // entry recorded before artifacts and references were told apart, which was
+ // always an artifact. Any other value is malformed and rejects the entry.
+ const isArtifact = raw['isArtifact'];
+ if (isArtifact !== undefined && typeof isArtifact !== 'boolean') {
+ return undefined;
+ }
const artifact: {
id: string;
type: SessionArtifactType;
label: string;
+ isArtifact: boolean;
link?: string;
uri?: string;
commitHash?: string;
isGitHub?: boolean;
- createdByThisSession?: boolean;
- } = { id: raw['id'], type: raw['type'], label: raw['label'] };
+ } = {
+ id: raw['id'],
+ type: raw['type'],
+ label: raw['label'],
+ isArtifact: isArtifact ?? true,
+ };
if (typeof raw['link'] === 'string') { artifact.link = raw['link']; }
if (typeof raw['uri'] === 'string') { artifact.uri = raw['uri']; }
if (typeof raw['commitHash'] === 'string') { artifact.commitHash = raw['commitHash']; }
if (typeof raw['isGitHub'] === 'boolean') { artifact.isGitHub = raw['isGitHub']; }
- if (typeof raw['createdByThisSession'] === 'boolean') { artifact.createdByThisSession = raw['createdByThisSession']; }
return artifact;
}
@@ -113,20 +130,39 @@ export function stringifySessionArtifacts(artifacts: readonly ISessionArtifact[]
return JSON.stringify(artifacts);
}
-/** Parses artifacts previously written by {@link stringifySessionArtifacts}. */
-export function parseSessionArtifacts(value: string | undefined): readonly ISessionArtifact[] {
- if (!value) {
- return [];
- }
- try {
- return readSessionArtifacts({ [SESSION_META_ARTIFACTS_KEY]: JSON.parse(value) });
- } catch {
- return [];
- }
+/** The outcome of reading persisted artifacts: what was read, and what was lost. */
+export interface IParsedSessionArtifacts {
+ readonly artifacts: readonly ISessionArtifact[];
+ /** Why nothing could be read, when the payload itself was unreadable. */
+ readonly error?: Error;
+ /** How many individual entries were rejected as malformed. */
+ readonly dropped: number;
}
/**
- * The value that identifies an artifact for de-duplication: its link, resource
+ * Parses artifacts previously written by {@link stringifySessionArtifacts}.
+ * Reports what could not be read rather than silently returning less, so a
+ * corrupt row does not empty a session's artifacts without a trace.
+ */
+export function parseSessionArtifacts(value: string | undefined): IParsedSessionArtifacts {
+ if (!value) {
+ return { artifacts: [], dropped: 0 };
+ }
+ let raw: unknown;
+ try {
+ raw = JSON.parse(value);
+ } catch (error) {
+ return { artifacts: [], error: error instanceof Error ? error : new Error(String(error)), dropped: 0 };
+ }
+ if (!Array.isArray(raw)) {
+ return { artifacts: [], error: new Error('expected an array of artifacts'), dropped: 0 };
+ }
+ const artifacts = readSessionArtifacts({ [SESSION_META_ARTIFACTS_KEY]: raw });
+ return { artifacts, dropped: raw.length - artifacts.length };
+}
+
+/**
+ * The value that identifies an entry for de-duplication: its link, resource
* URI or commit hash, normalized for comparison.
*/
export function getSessionArtifactValue(artifact: ISessionArtifact): string {
diff --git a/src/vs/platform/agentHost/common/sessionDataService.ts b/src/vs/platform/agentHost/common/sessionDataService.ts
index 7f78220de4d..6e050039e56 100644
--- a/src/vs/platform/agentHost/common/sessionDataService.ts
+++ b/src/vs/platform/agentHost/common/sessionDataService.ts
@@ -168,6 +168,18 @@ export interface ISessionDatabase extends IDisposable {
*/
getTurnUsages(): Promise