mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-07 07:35:00 +01:00
Merge origin/main into chrmarti/bad-earthworm
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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`.
|
||||
|
||||
|
||||
@@ -257,10 +257,9 @@ vocabulary that lets an agreed design be built consistently**, not as the openin
|
||||
move in a review. Reach for them *after* you've named the feeling and the
|
||||
principle, never instead of it.
|
||||
|
||||
The size tokens live in
|
||||
[`baseSizes.ts`](../../../src/vs/platform/theme/common/sizes/baseSizes.ts) and the
|
||||
font ramp in [`sizes.ts`](../../../src/vs/sessions/common/sizes.ts); the full
|
||||
reference is in
|
||||
The size and font tokens live in
|
||||
[`baseSizes.ts`](../../../src/vs/platform/theme/common/sizes/baseSizes.ts); the
|
||||
full reference is in
|
||||
[design-tokens.instructions.md](../../instructions/design-tokens.instructions.md).
|
||||
|
||||
<a id="design-tokens"></a>
|
||||
|
||||
@@ -81,7 +81,7 @@ the schema's nested
|
||||
|
||||
| Schema property (path) | Type in schema | Composition (`x-composition.strategy`) |
|
||||
|------------------------|----------------|----------------------------------------|
|
||||
| `permissions.disableBypassPermissionsMode` | string enum `"disable"` | most-restrictive-wins (sticky once set) |
|
||||
| `permissions.disableBypassPermissionsMode` | string enum `"disable"` \| `"allow-auto-only"` | most-restrictive-wins (sticky once set) |
|
||||
| `model` | string (`auto`, a model family name, or a full model id) | — |
|
||||
| `permissions.model` | string (legacy location for `model`) | — |
|
||||
| `forceRemoteSettingsRefresh` | boolean | MDM wins; controls the server cache rather than a configuration setting |
|
||||
@@ -372,10 +372,13 @@ constant, configuration policy, or policy-data export.
|
||||
|
||||
`forceRemoteSettingsRefresh` is not a user configuration setting. It controls whether the
|
||||
server-managed-settings cache may satisfy startup, so VS Code preserves it in the cached raw server
|
||||
bag and always includes it in the native MDM watch schema. `DefaultAccountProvider` resolves an
|
||||
explicit native MDM boolean ahead of the cached server value; when the result is `true`, it bypasses
|
||||
an otherwise-fresh server cache for the first fetch for that account in the current process. The
|
||||
cache remains available as the normal fetch-failure fallback.
|
||||
bag and always includes it in the native MDM watch schema. `DefaultAccountProvider` resolves the
|
||||
control across native MDM, cached server, and managed-file delivery before using the server cache.
|
||||
When the result is `true`, only a fresh successful server response for the current account,
|
||||
authentication provider, and endpoint satisfies the requirement. A failed refresh may retain cached
|
||||
restrictions and the flag itself, but the Account Policy gate keeps AI features disabled until a
|
||||
retry succeeds. Authentication remains available so users can recover from missing or expired
|
||||
credentials.
|
||||
|
||||
Reference tests:
|
||||
- `src/vs/platform/policy/test/common/copilotManagedSettings.test.ts`
|
||||
|
||||
@@ -25,11 +25,22 @@ Choose the client setup in the GUI:
|
||||
- **Code OSS from sources:** apply `product.overrides.json`, reload, sign in, and
|
||||
run **Developer: Sync Account Policy**.
|
||||
- **Stable, Insiders, CLI, or other clients:** configure the displayed system
|
||||
proxy mapping.
|
||||
proxy mapping and enable Proxyman's platform proxy toggle (**Tools > macOS
|
||||
Proxy** or **Tools > Override Windows Proxy**). VS Code clients must also add
|
||||
the displayed `http.proxy` property to `settings.json`.
|
||||
|
||||
Use **Clear Policy Cache** when the runtime's fresh managed-settings cache
|
||||
prevents a network request. The live request log confirms whether the client
|
||||
reached the server.
|
||||
Use **Clear SDK Policy Cache**, expand the macOS or Windows section, and run the
|
||||
copied command when the runtime's fresh managed-settings cache prevents a network
|
||||
request. Select a known policy endpoint in the live request log to open its
|
||||
response editor.
|
||||
|
||||
To test `forceRemoteSettingsRefresh` fail-closed behavior, apply the
|
||||
`customization-lockdown` managed-settings preset and sync once successfully.
|
||||
Then select the `server-error` preset or choose the `malformed-json`,
|
||||
`disconnect`, or `timeout` response behavior and sync again. The successful
|
||||
first response seeds the cached refresh requirement; the second response
|
||||
exercises HTTP, parse, immediate-network, or client-timeout failure without
|
||||
manually editing payloads.
|
||||
|
||||
Other Copilot clients share the default cache. For deterministic testing, start
|
||||
both Code OSS and the mock server with the same isolated `COPILOT_CACHE_HOME`.
|
||||
|
||||
@@ -1,27 +1,120 @@
|
||||
---
|
||||
name: update-screenshots
|
||||
description: Download screenshot baselines from the latest CI run and commit them. Use when asked to update, accept, or refresh component screenshot baselines from CI, or after the screenshot-test GitHub Action reports differences. This skill should be run as a subagent.
|
||||
description: Update the committed blocks-ci screenshot hashes after the "Screenshots & Tests" check fails, or investigate a screenshot diff reported on a PR. Use when asked to update, accept, or refresh component screenshot baselines from CI. This skill should be run as a subagent.
|
||||
---
|
||||
|
||||
# Update Component Screenshots from CI
|
||||
|
||||
Screenshot baselines are **no longer stored in the repository**. They are managed by an external screenshot service (`hediet-screenshots.azurewebsites.net`). The CI workflow uploads screenshots to this service and diffs them automatically.
|
||||
Screenshot **images** are not stored in the repository — they live in an external service
|
||||
(`hediet-screenshots.azurewebsites.net`), keyed by commit SHA. But a subset of fixtures is
|
||||
pinned by **hash** in [`test/componentFixtures/blocks-ci-screenshots.md`](../../../test/componentFixtures/blocks-ci-screenshots.md),
|
||||
and that file **is** committed. When those hashes change, CI fails and you must update the file.
|
||||
|
||||
When the `Checking Component Screenshots` GitHub Action detects changes, it posts a PR comment with before/after comparisons. No manual baseline updates are needed — the screenshots on the `main` branch commit become the new baselines automatically after merge.
|
||||
## Two different outcomes, only one of which blocks
|
||||
|
||||
## What Changed
|
||||
The `Screenshots & Tests` job in [`.github/workflows/component-fixtures.yml`](../../workflows/component-fixtures.yml)
|
||||
produces two independent results:
|
||||
|
||||
- Baseline images were removed from `test/componentFixtures/.screenshots/baseline/`.
|
||||
- Git LFS is no longer used for screenshot storage.
|
||||
- The screenshot service stores images keyed by commit SHA and handles diffing.
|
||||
| Result | Blocking? | Action |
|
||||
| --- | --- | --- |
|
||||
| Screenshot **diff report** (PR comment with before/after images) | No — informational | Review the visuals. Nothing to commit. |
|
||||
| **blocks-ci hash mismatch** | **Yes — fails the check** | Update `blocks-ci-screenshots.md` and commit. |
|
||||
|
||||
## If Screenshots Need Investigation
|
||||
A fixture opts into the blocking gate with `labels: { kind: 'screenshot', blocksCi: true }`.
|
||||
Only those fixtures appear in `blocks-ci-screenshots.md`.
|
||||
|
||||
1. Check the PR comment posted by the CI workflow for visual diffs.
|
||||
2. Download the `screenshots` artifact from the CI run for the raw captured images:
|
||||
The failure looks like this:
|
||||
|
||||
```bash
|
||||
gh run download <run-id> --name screenshots --dir .tmp/screenshots
|
||||
```
|
||||
##[error]blocks-ci screenshot hashes do not match committed file. See PR comment or job summary for the updated content.
|
||||
```
|
||||
|
||||
3. Compare locally if needed. The artifact contains the full set of captured screenshots.
|
||||
## Step 1: Get the expected hashes from CI
|
||||
|
||||
> **Never regenerate the hashes locally.** They are hashes of the rendered PNG bytes, produced
|
||||
> on `ubuntu-latest`. Rendering on macOS or Windows yields different bytes and therefore
|
||||
> different hashes, so locally generated values will fail CI. Always copy the values from the
|
||||
> CI job.
|
||||
|
||||
Three surfaces carry the same content — use whichever is handy:
|
||||
|
||||
- The **PR comment** titled "blocks-ci screenshots changed" (non-fork PRs only) — contains the
|
||||
full updated file plus a patch.
|
||||
- The **job summary**, which gets the identical body and is the only surface fork PRs receive.
|
||||
- The **job log**, whose final step prints a unified diff:
|
||||
|
||||
```bash
|
||||
gh api repos/microsoft/vscode/actions/jobs/<JOB_ID>/logs > "$TMPDIR/ci-job-log.txt"
|
||||
grep -n '##\[error\]' "$TMPDIR/ci-job-log.txt"
|
||||
```
|
||||
|
||||
Find the failed job id with:
|
||||
|
||||
```bash
|
||||
gh pr checks <PR> --json name,link,bucket --jq '.[] | select(.name == "Screenshots & Tests")'
|
||||
```
|
||||
|
||||
## Step 2: Verify the change is intentional before accepting it
|
||||
|
||||
This gate exists to catch **unintended** layout regressions, so accepting new hashes without
|
||||
looking at the images defeats its purpose. The images are publicly fetchable by hash, so pull
|
||||
both the old (committed) and new (from CI) versions and compare:
|
||||
|
||||
```bash
|
||||
curl -sL -o old.png "https://hediet-screenshots.azurewebsites.net/images/<OLD_HASH>"
|
||||
curl -sL -o new.png "https://hediet-screenshots.azurewebsites.net/images/<NEW_HASH>"
|
||||
```
|
||||
|
||||
Then view them, and localize the change rather than eyeballing full screenshots — the delta is
|
||||
often only a pixel or two:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
from PIL import Image, ImageChops
|
||||
a = Image.open('old.png').convert('RGB'); b = Image.open('new.png').convert('RGB')
|
||||
print('diff bbox:', ImageChops.difference(a, b).getbbox())
|
||||
"
|
||||
```
|
||||
|
||||
Confirm the delta matches what the PR intends. If the fixture is unrelated to the change, or
|
||||
the shift is larger than expected, treat it as a regression and fix the code instead of the
|
||||
hashes.
|
||||
|
||||
## Step 3: Apply and commit
|
||||
|
||||
Edit only the changed lines in `test/componentFixtures/blocks-ci-screenshots.md`, replacing the
|
||||
old hash in the image URL with the new one:
|
||||
|
||||
```md
|
||||
#### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark
|
||||

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