diff --git a/.cnb.yml b/.cnb.yml new file mode 100644 index 0000000000000..cf291737cb69a --- /dev/null +++ b/.cnb.yml @@ -0,0 +1,64 @@ +# .cnb.yml +.npc: &npc + - services: + - docker + stages: + - name: run with npc + image: cnbcool/default-npc-agent:latest + +好大个: + issue.comment@npc: *npc + pull_request.comment@npc: *npc + +$: + vscode: + - runner: + cpus: 32 + docker: + build: .ide/Dockerfile + env: + # 声明首次打开终端时自动执行的命令,支持多行文本 + CNB_WELCOME_CMD: | + echo "Hello World!" + echo done + services: + - vscode + - docker + # 开发环境启动后会执行的任务 + stages: + - name: ls + script: ls -al + tag_push: + - stages: + - name: 生成 Code Wiki + timeout: 4h + image: cnbcool/codewiki:latest + settings: + git_doc_dir: /${CNB_BUILD_WORKSPACE}/${CNB_REPO_SLUG}/codewiki + +# .cnb.yml +"**": # 触发的分支名,默认所有分支,可按需修改 + push: # push 触发,可按需修改为 pull_request 等 + - stages: + # 代码分析 + - name: TCA + timeout: 2h + image: cnbcool/tca:latest + settings: + from_file: # 可选,可以传递一个文件(比如 changed.txt),内容为需要分析的文件列表,一行一个文件(采用基于工作空间的相对路径) + ignore_paths: # 可选,指定一个或多个相对工作空间的文件屏蔽路径(黑名单),多个路径可写成数组格式,路径采用Unix通配符格式。 + white_paths: # 可选,指定一个或多个相对工作区的扫描路径(白名单),多个路径可写成数组格式,路径采用Unix通配符格式。 + block: # 可选,默认为true, 如果有代码问题或分析异常,会阻塞流水线。如果不希望阻塞流水线,可以设置为false。 + + - stages: + - name: build knowledge base + image: cnbcool/knowledge-base + settings: + include: '**/*.md,**/*.mdx,**/*.pdf,**/*.docx,**/*.txt' + issue_sync_enabled: true + issue_labels: + - bug + - feature + issue_state: "open" + issue_priority: "P0,P1" + diff --git a/.cnb/settings.yml b/.cnb/settings.yml new file mode 100644 index 0000000000000..3f24b3a409bb6 --- /dev/null +++ b/.cnb/settings.yml @@ -0,0 +1,32 @@ +npc: + roles: + - name: 好大个 + prompt: | + 你用"好大个"自称,叫用户"大人", + 你的口头禅是『此事必有蹊跷!』, + 结束对话前礼貌地回复一行:"此事背后一定有一个天大的秘密。" + 无论是日常对话还是讲解知识,你都会保持以上风格, + 使用中文的『~』代替所有英文的『~』, + 用卑微并专业的语气回答接下来的问题 + + +# 云原生开发配置 +# 注意:读取云原生启动按钮所在页面当前分支的 .cnb/settings.yml 配置 +workspace: + launch: + # 定制云原生开发启动按钮 + button: + # 按钮名称 + name: 启动云原生开发 + # 按钮描述 + # 如果值为 null,则不显示默认描述 + description: 点击此按钮启动云原生开发环境 + # 鼠标悬浮图片:仓库图片填相对路径(如 .cnb/launch-hover.gif),外部图片填 raw 地址,最大 10MB + # hoverImage: .cnb/launch-hover.gif + # CPU 核心数,默认 8,仅对默认模板有效 + cpus: 16 + # 是否禁用默认按钮,默认 false + disabled: false + # 环境创建后是否自动打开 WebIDE,默认 false + # 未安装 openssh 时会自动打开,不受此参数影响 + autoOpenWebIDE: true \ No newline at end of file diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index 5d1beb9488986..efc9a0d40df92 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -9,6 +9,11 @@ "version": "1.5.0", "resolved": "ghcr.io/devcontainers/features/rust@sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c", "integrity": "sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c" + }, + "ghcr.io/devcontainers/features/sshd:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/sshd@sha256:f5251b8e4325f68f7280973c6cd65daff414449c66f240621502d4e8e74eb7ee", + "integrity": "sha256:f5251b8e4325f68f7280973c6cd65daff414449c66f240621502d4e8e74eb7ee" } } } \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 947527eb4c90f..874a814fbbfa6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,8 @@ }, "features": { "ghcr.io/devcontainers/features/desktop-lite:": {}, - "ghcr.io/devcontainers/features/rust:": {} + "ghcr.io/devcontainers/features/rust:": {}, + "ghcr.io/devcontainers/features/sshd:1": {} }, "containerEnv": { "DISPLAY": "" // Allow the Dev Containers extension to set DISPLAY, post-create.sh will add it back in ~/.bashrc and ~/.zshrc if not set. diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index 35ea29b51742d..dfeb89b450764 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -4,12 +4,12 @@ src/vs/base/browser/domSanitize.ts @mjbvz src/vs/base/parts/quickinput/** @TylerLeonhardt # Base Widgets -src/vs/base/browser/ui/grid/** @joaomoreno @benibenj -src/vs/base/browser/ui/list/** @joaomoreno @benibenj -src/vs/base/browser/ui/sash/** @joaomoreno @benibenj -src/vs/base/browser/ui/splitview/** @joaomoreno @benibenj -src/vs/base/browser/ui/table/** @joaomoreno @benibenj -src/vs/base/browser/ui/tree/** @joaomoreno @benibenj +src/vs/base/browser/ui/grid/** @benibenj +src/vs/base/browser/ui/list/** @benibenj +src/vs/base/browser/ui/sash/** @benibenj +src/vs/base/browser/ui/splitview/** @benibenj +src/vs/base/browser/ui/table/** @benibenj +src/vs/base/browser/ui/tree/** @benibenj # Platform src/vs/platform/browserView/** @kycutler @jruales @@ -43,7 +43,7 @@ src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @TylerLeonha src/vs/workbench/contrib/scm/** @lszomoru src/vs/workbench/contrib/terminal/** @anthonykim1 src/vs/workbench/contrib/terminalContrib/** @anthonykim1 -src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @alexr00 @joaomoreno +src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @alexr00 src/vs/workbench/contrib/preferences/** @rzhao271 # Sessions Contributions diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7e6bf875307c3..2904f679f73c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,14 +1,14 @@ # GitHub actions required reviewers -.github/workflows/monaco-editor.yml @hediet @alexdima @lszomoru @joaomoreno -.github/workflows/no-package-lock-changes.yml @lszomoru @alexdima @joaomoreno @TylerLeonhardt @rzhao271 @Yoyokrazy -.github/workflows/no-yarn-lock-changes.yml @lszomoru @alexdima @joaomoreno @TylerLeonhardt @rzhao271 @Yoyokrazy -.github/workflows/pr-darwin-test.yml @lszomoru @alexdima @joaomoreno @TylerLeonhardt @rzhao271 @Yoyokrazy -.github/workflows/pr-linux-cli-test.yml @lszomoru @alexdima @joaomoreno @TylerLeonhardt @rzhao271 @Yoyokrazy -.github/workflows/pr-linux-test.yml @lszomoru @alexdima @joaomoreno @TylerLeonhardt @rzhao271 @Yoyokrazy -.github/workflows/pr-node-modules.yml @lszomoru @alexdima @joaomoreno @TylerLeonhardt @rzhao271 @Yoyokrazy -.github/workflows/pr-win32-test.yml @lszomoru @alexdima @joaomoreno @TylerLeonhardt @rzhao271 @Yoyokrazy -.github/workflows/pr.yml @lszomoru @alexdima @joaomoreno @TylerLeonhardt @rzhao271 @Yoyokrazy -.github/workflows/telemetry.yml @lramos15 @lszomoru @alexdima @joaomoreno +.github/workflows/monaco-editor.yml @hediet @alexdima @lszomoru +.github/workflows/no-package-lock-changes.yml @lszomoru @alexdima @sandy081 @TylerLeonhardt @rzhao271 @Yoyokrazy +.github/workflows/no-yarn-lock-changes.yml @lszomoru @alexdima @sandy081 @TylerLeonhardt @rzhao271 @Yoyokrazy +.github/workflows/pr-darwin-test.yml @lszomoru @alexdima @sandy081 @TylerLeonhardt @rzhao271 @Yoyokrazy +.github/workflows/pr-linux-cli-test.yml @lszomoru @alexdima @sandy081 @TylerLeonhardt @rzhao271 @Yoyokrazy +.github/workflows/pr-linux-test.yml @lszomoru @alexdima @sandy081 @TylerLeonhardt @rzhao271 @Yoyokrazy +.github/workflows/pr-node-modules.yml @lszomoru @alexdima @sandy081 @TylerLeonhardt @rzhao271 @Yoyokrazy +.github/workflows/pr-win32-test.yml @lszomoru @alexdima @sandy081 @TylerLeonhardt @rzhao271 @Yoyokrazy +.github/workflows/pr.yml @lszomoru @alexdima @sandy081 @TylerLeonhardt @rzhao271 @Yoyokrazy +.github/workflows/telemetry.yml @lramos15 @lszomoru @alexdima # VS Code API # Ensure the API team is aware of changes to the vscode-dts file diff --git a/.github/commands.json b/.github/commands.json index 461fd84f07837..4728611887ac0 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -82,7 +82,7 @@ "name": "*caused-by-extension", "action": "close", "reason": "not_planned", - "comment": "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). If you don't know which extension is causing the problem, you can run `Help: Start extension bisect` from the command palette (F1) to help identify the problem extension.\n\nHappy Coding!" + "comment": "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). If you don't know which extension is causing the problem, you can run `Help: Start Extension Bisect` from the command palette (F1) to help identify the problem extension.\n\nHappy Coding!" }, { "type": "label", diff --git a/.github/instructions/best-practices.instructions.md b/.github/instructions/best-practices.instructions.md index e20eb82720876..006c8f2d387eb 100644 --- a/.github/instructions/best-practices.instructions.md +++ b/.github/instructions/best-practices.instructions.md @@ -24,6 +24,15 @@ applyTo: src/vs/** - Don't hardcode URI scheme strings like `'file'`, `'untitled'`, or `'vscode-remote'`. Use the `Schemas` constants from `vs/base/common/network.ts` (e.g. `Schemas.file`, `Schemas.untitled`, `Schemas.vscodeRemote`). - Don't compare URIs with `===` or `uri.toString()`. Use the comparison utilities from `vs/base/common/resources.ts`: `isEqual` for equality, `isEqualOrParent` for containment, and `getComparisonKey` when a URI is used as a map/set key. These handle path-case sensitivity and fragment/authority correctly. When you need explicit control over case sensitivity, use an `ExtUri` instance (`extUri`, `extUriIgnorePathCase`, or `extUriBiasedIgnorePathCase`) instead of the bound helpers. +## Resource Labels + +- Don't set `{ supportIcons: true }` when creating a `ResourceLabel` (`vs/workbench/browser/labels.ts`). This option makes the label parse `$(codicon)` syntax in the name/description and is only needed when you want to render a codicon inline with the resource text. By default (without it), the label computes the proper file-icon CSS classes for the resource, which is what we almost always want. Note that those classes only render as icons when an ancestor DOM element enables file icons (see below); otherwise the label shows text only. +- To actually display file icons for resource labels in a tree/list, an ancestor container must have the `show-file-icons` class and be wired to the active file icon theme. Don't add the class by hand — call `createFileIconThemableTreeContainerScope` (`vs/workbench/contrib/files/browser/views/explorerView.ts`) on the container. It adds the required `show-file-icons` / `file-icon-themable-tree` classes and keeps `align-icons-and-twisties` / `hide-arrows` in sync with the file icon theme. Register the returned `IDisposable`. A common bug is placing a resource-label list/tree outside such a scoped container, which makes file icons silently disappear. + +## Styling + +- Avoid `getComputedStyle`. If a style value is needed in both CSS and TypeScript, prefer hardcoding the value in TypeScript and setting it directly on the DOM element (e.g. `element.style.width = '100px'`), or set a CSS custom property via `element.style.setProperty('--my-var', value)` when the value is needed across multiple CSS rules. + ## Editor Decorations - For editor highlights, use a regular editor decoration with an `inlineClassName` or `className` plus a CSS rule. diff --git a/.github/instructions/coding-guidelines.instructions.md b/.github/instructions/coding-guidelines.instructions.md index 17f9662b87cb5..cbab74790541f 100644 --- a/.github/instructions/coding-guidelines.instructions.md +++ b/.github/instructions/coding-guidelines.instructions.md @@ -28,6 +28,11 @@ Use tabs, not spaces. - Use JSDoc style comments for functions, interfaces, enums, and classes - Do not write large comments in the middle of a method or comments that explain a single line. If a line or block needs a paragraph of explanation to be understood, treat that as a signal that the code itself is unclear: extract a well-named function, introduce an explanatory variable, or simplify the logic instead. Keep any remaining inline comment to a brief note. +- **Default to NO comment.** Add one only when the code cannot be made self-explanatory by naming. Code that is obvious from its identifiers must not be commented. +- **Hard limits** (treat as rules, not suggestions): + - JSDoc on a function/interface/property: at most 1–2 short sentences. Do not enumerate every branch/feature, restate the signature, list what it does NOT do, or explain parameters that are obvious from their names/types. + - Inline comment inside a method body: at most 1 line, and only for a genuine workaround/hack, a non-obvious ordering constraint, or a surprising side effect. Never narrate the next statement (e.g. `// Expand the variable`, `// loop over args`). +- Before writing any comment longer than one line, stop and either delete it or shorten it to a single line. A multi-line block comment inside a method body is almost always wrong. ## Strings diff --git a/.github/instructions/oss-third-party-notices.instructions.md b/.github/instructions/oss-third-party-notices.instructions.md index 1af566c41ac77..420e42f28bae0 100644 --- a/.github/instructions/oss-third-party-notices.instructions.md +++ b/.github/instructions/oss-third-party-notices.instructions.md @@ -1,10 +1,12 @@ --- -applyTo: 'build/azure-pipelines/oss/**' +applyTo: 'build/azure-pipelines/{oss/**,common/downloadNotice.ts,{win32,linux,darwin}/steps/product-build-*-compile.yml}' --- # VS Code OSS Third-Party-Notices pipeline -This directory contains the thin VS Code-specific layer around Component Governance (CG) for producing OSS third-party notices. The goal is to replace the legacy custom OSS tool with CG plus a gap-filling scanner, so release owners do not need to babysit `ThirdPartyNotices.txt` every release. The generated output should be at least as good as the legacy tool: CG provides the base NOTICE, the local scanner fills known CG gaps, and human-authored overrides cover the small set that automation cannot safely resolve. +This directory contains the thin VS Code-specific layer around Component Governance (CG) for producing OSS third-party notices. It replaces the legacy custom OSS tool with CG plus a gap-filling scanner, so release owners do not need to babysit `ThirdPartyNotices.txt` every release. The generated output should be at least as good as the legacy tool: CG provides the base NOTICE, the local scanner fills known CG gaps, and human-authored overrides cover the small set that automation cannot safely resolve. + +The pipeline has two halves: **generation** (this `oss/` directory — CG + scanner + merge, producing the `notice_output` artifact) and **application** (the cutover that swaps the merged notice into the shipped product — see "Applying the NOTICE (cutover)" below). ## Architecture @@ -34,6 +36,45 @@ In `product-quality-checks.yml`: 6. `merge-notices.js` runs with `--cg`, `--extensions`, `--cglicenses`, and `--output`. 7. The generated CG file, scanner file, final merged file, and optional cache metadata are uploaded under `notice_output`. +## Applying the NOTICE (cutover) + +Generation (above) produces the merged `ThirdPartyNotices.new.txt` in the `notice_output` artifact. **Application** is the cutover that makes that file the one VS Code actually ships, replacing the legacy mixin notice. + +Consumer script: `build/azure-pipelines/common/downloadNotice.ts` (read its header comment for the full design rationale). + +How it works, per desktop platform compile template (`build/azure-pipelines/{win32,linux,darwin}/steps/product-build-*-compile.yml`): + +1. **Pull CG NOTICE (background)** — at compile start, `deemon --detach` launches `downloadNotice.ts` as a detached poller. It polls the parallel Quality stage's `notice_output` artifact while compilation runs, so the wait overlaps work already happening. +2. **Apply CG NOTICE** — right before gulp packages the app, `deemon --attach` blocks on that poller. It downloads, extracts, validates the merged notice, and **overwrites the repo-root `ThirdPartyNotices.txt`**. `build/gulpfile.vscode.ts` then packages whatever file is at that path → the CG notice ships. + +Only the 7 desktop targets that bundle a notice run these steps (win32 x64/arm64, darwin universal, linux x64/arm64/armhf). REH/server/web/alpine are unaffected. + +### Fallback chain (never fail the build) + +`downloadNotice.ts` is **non-fatal — it always exits 0.** A notice problem must never break packaging. The outcomes, in order: + +1. **CG fresh** — the `notice_output` artifact is present and valid → overwrite with it. +2. **Cached CG** — if CG generation failed upstream, the Quality stage substitutes the last good `ThirdPartyNotices.generated.txt` (same branch, then `main`) before the artifact is published — so the consumer still gets a CG notice. +3. **Legacy** — if no usable artifact is available, the legacy mixin notice that `mixin-quality.ts` already laid down is left in place. `mixin-quality.ts` is deliberately left untouched (it's a shared chokepoint used by 8+ pipelines), which is what guarantees we never ship with *no* notice. + +The accept-gate validates the *extracted content* (file present AND non-trivial) inside the poll loop — it does not trust the artifact listing alone. A mid-upload miss re-polls rather than falling back, which prevents a "mixed notice" race where one platform ships legacy while its siblings ship CG. + +### Rollback lever + +A queue-time checkbox **"Use legacy OSS Notice"** (parameter `VSCODE_USE_LEGACY_OSS_NOTICE`, default `false`) is the instant rollback. When checked, it derives `VSCODE_OVERWRITE_TPN=false`, and `downloadNotice.ts` skips the overwrite so the legacy notice ships — no code change or redeploy needed. + +> ⚠️ `downloadNotice.ts` normalizes the flag with `.trim().toLowerCase()` before comparing, so YAML casing (`true`/`false` vs `True`/`False`) can't break the rollback lever. The pipeline still derives `VSCODE_OVERWRITE_TPN` as a lowercase string literal (`'true'`/`'false'`) for clarity. See the comment in `product-build-variables.yml`. + +### Diagnosing a build + +`downloadNotice.ts` logs three greppable markers so a build log answers "did the cutover work?": + +- `[notice-cutover] RESULT=fresh` — overwrote from `notice_output` (the happy path). +- `[notice-cutover] RESULT=fallback` — artifact missing → kept the legacy notice. +- `[notice-cutover] RESULT=disabled` — feature flag off → kept the legacy notice. + +To confirm a shipped artifact carries the CG notice, the root `resources/app/ThirdPartyNotices.txt` should be the large CG-merged file (multi-MB) rather than the ~3 MB legacy notice. Validate *intra-build* parity — all platforms in one build should produce a byte-identical notice (same SHA-256) — rather than checking against a fixed byte count, since CG's exact output size varies run-to-run. + ## Script reference ### `scan-licenses.ts` diff --git a/.github/instructions/resources/interactive/interactive.editor.drawio.svg b/.github/instructions/resources/interactive/interactive.editor.drawio.svg index c6b947982800c..1a19b6e02245a 100644 --- a/.github/instructions/resources/interactive/interactive.editor.drawio.svg +++ b/.github/instructions/resources/interactive/interactive.editor.drawio.svg @@ -1,331 +1 @@ - - - - - - - - - - - - - - - Notebook Editor Widget - - - - - - Notebook Editor Widget - - - - - - - - - - - Code Editor Widget - - - - - - Code Editor Widget - - - - - - - - - - - Interactive Editor - - - - - - Interactive Editor - - - - - - - - - - - - NotebookService - - - - - - NotebookService - - - - - - - - - - - - TextModelResolverService - - - - - - TextModelResolverService - - - - - - - - - - - - - - - - - - NotebookTextModel - - - - - - NotebookTextModel - - - - - - - - - - - ITextModel - - - - - - ITextModel - - - - - - - - - - - Interactive Editor Input - - - - - - Interactive Editor In... - - - - - - - - - - - EditorPane - - - - - - EditorPane - - - - - - - - - - - EditorInput - - - - - - EditorInput - - - - - - - - - - - - - Editor Resolver Service - - - - - - Editor Resolver Service - - - - - - - - - - - EditorPane Registry - - - - - - EditorPane Registry - - - - - - - - - - - - - Registry Editor Pane - - - - - - Registry Editor Pane - - - - - - - - - - - - - - - - registerEditor - - - - - - - registerEditor - - - - - - - - - - - - - input: EditorInput - - - - - - input: EditorInput - - - - - - - - - - - - - group: IEditorGroup - - - - - - group: IEditorGroup - - - - - - - - - - - - - getControl: IEditorControl - - - - - - getControl: IEditorControl - - - - - - - - - Text is not SVG - cannot display - - - - +Notebook Editor WidgetNotebook Editor WidgetCode Editor WidgetCode Editor WidgetInteractive EditorInteractive EditorNotebookServiceNotebookServiceTextModelResolverServiceTextModelResolverServiceNotebookTextModelNotebookTextModelITextModelITextModelInteractive Editor InputInteractive Editor In...EditorPaneEditorPaneEditorInputEditorInputEditor Resolver ServiceEditor Resolver ServiceEditorPane RegistryEditorPane RegistryRegistry Editor PaneRegistry Editor PaneregisterEditorregisterEditorinput: EditorInputinput: EditorInputgroup: IEditorGroupgroup: IEditorGroupgetControl: IEditorControlgetControl: IEditorControlText is not SVG - cannot display \ No newline at end of file diff --git a/.github/instructions/resources/interactive/interactive.eh.drawio.svg b/.github/instructions/resources/interactive/interactive.eh.drawio.svg index 5f9f40795c2b1..71662103ad74f 100644 --- a/.github/instructions/resources/interactive/interactive.eh.drawio.svg +++ b/.github/instructions/resources/interactive/interactive.eh.drawio.svg @@ -1,244 +1 @@ - - - - - - - - - - - - - - Ext Host - - - - - - - Ext Host - - - - - - - - - - - - UI - - - - - - - UI - - - - - - - - - - - - - - Notebook Editor - - - - - - Notebook Editor - - - - - - - - - - - Text Editor - - - - - - Text Editor - - - - - - - - - - - Interactive Editor - - - - - - Interactive Editor - - - - - - - - - - - - NotebookService - - - - - - NotebookService - - - - - - - - - - - - - - TextModelResolverService - - - - - - TextModelResolverService - - - - - - - - - - - - - - ExthostNotebook - - - - - - ExthostNotebook - - - - - - - - - - - - - - ExthostEditors - - - - - - ExthostEditors - - - - - - - - - - - - - - - ExthostInteractive - - - - - - ExthostInteracti... - - - - - - - - - - - - - NotebookEditor - - - - - - NotebookE... - - - - - - - - - - - - - Input - - - - - - Input - - - - - - - - - Text is not SVG - cannot display - - - - +Ext HostExt HostUIUINotebook EditorNotebook EditorText EditorText EditorInteractive EditorInteractive EditorNotebookServiceNotebookServiceTextModelResolverServiceTextModelResolverServiceExthostNotebookExthostNotebookExthostEditorsExthostEditorsExthostInteractiveExthostInteracti...NotebookEditorNotebookE...InputInputText is not SVG - cannot display \ No newline at end of file diff --git a/.github/instructions/resources/interactive/interactive.model.resolution.drawio.svg b/.github/instructions/resources/interactive/interactive.model.resolution.drawio.svg index 61c91b981255e..742ff1d618556 100644 --- a/.github/instructions/resources/interactive/interactive.model.resolution.drawio.svg +++ b/.github/instructions/resources/interactive/interactive.model.resolution.drawio.svg @@ -1,352 +1 @@ - - - - - - - - - - - - - - Read resource - - - - - - Read resource - - - - - - - - - - - - - Deserialize NotebookData - - - - - - Deserialize NotebookData - - - - - - - - - - - - - creates - - - - - - creates - - - - - - - - - - - - creates - - - - - - creates - - - - - - - - - - - - creates - - - - - - creates - - - - - - - - - - - - NotebookEditorModelResolverService - - - - - - NotebookEditorMod... - - - - - - - - - - - - SimpleNotebookEditorModel - - - - - - - SimpleNoteboookEd... - - - - - - - - - - - - NotebookFileWorkingCopyModelFactory - - - - - - - NotebookFileWorki... - - - - - - - - - - - - WorkingCopyManager - - - - - - - WorkingCopyManager - - - - - - - - - - - - creates - - - - - - creates - - - - - - - - - - - NotebookService - - - - - - NotebookService - - - - - - - - - - - FileService - - - - - - FileService - - - - - - - - - - - NotebookTextModel - - - - - - NotebookTextModel - - - - - - - - - - - - register FS provider - - for vscode-interactive schema - - - - - - register FS provider... - - - - - - - - - - - - register notebook serializer - - for interactive viewtype - - - - - - register notebook serializer... - - - - - - - - - - - interactive.contribution - - (startup) - - - - - - interactive.contributio... - - - - - - - - - - - - - - InteractiveEditor - - - - - - InteractiveEditor - - - - - - - - - - - - - - - NotebookEditorInput - - - - - - - NotebookEditorInp... - - - - - - - - - Text is not SVG - cannot display - - - - +Read resourceRead resourceDeserialize NotebookDataDeserialize NotebookDatacreatescreatescreatescreatescreatescreatesNotebookEditorModelResolverServiceNotebookEditorMod...SimpleNotebookEditorModelSimpleNoteboookEd...NotebookFileWorkingCopyModelFactoryNotebookFileWorki...WorkingCopyManagerWorkingCopyManagercreatescreatesNotebookServiceNotebookServiceFileServiceFileServiceNotebookTextModelNotebookTextModelregister FS providerfor vscode-interactive schemaregister FS provider...register notebook serializerfor interactive viewtyperegister notebook serializer...interactive.contribution(startup)interactive.contributio...InteractiveEditorInteractiveEditorNotebookEditorInputNotebookEditorInp...Text is not SVG - cannot display \ No newline at end of file diff --git a/.github/instructions/resources/notebook/cell-resize-above-viewport.drawio.svg b/.github/instructions/resources/notebook/cell-resize-above-viewport.drawio.svg index 6a2f80fc98da0..a17da1c03679a 100644 --- a/.github/instructions/resources/notebook/cell-resize-above-viewport.drawio.svg +++ b/.github/instructions/resources/notebook/cell-resize-above-viewport.drawio.svg @@ -1,654 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Notebook List View - - - - - - Notebook List View - - - - - - - - - - - Webview top -1000 - - - - - - Webview top -1000 - - - - - - - - - - - - - Viewport - - - - - - Viewport - - - - - - - - - - - - - - - - Notebook List View - - - - - - Notebook List View - - - - - - - - - - - - - - - - - - - - - - - Grow Height by 50 - - - - - - Grow Height by 50 - - - - - - - - - - - scrollTop 1000 - - - - - - scrollTop 1000 - - - - - - - - - - - scrollTop 1000 - - - - - - scrollTop 1000 - - - - - - - - - - - - - - - - - - - Notebook List View - - - - - - Notebook List View - - - - - - - - - - - - - - - - - - - - - - - scrollTop 1050 - - - - - - scrollTop 1050 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Notebook List View - - - - - - Notebook List View - - - - - - - - - - - - - - - - - - - - - - - scrollTop 1050 - - - - - - scrollTop 1050 - - - - - - - - - - - - - Adjust top - - - - - - Adjust top - - - - - - - - - - - - - UpdateScrollTop - - - - - - UpdateScrollTop - - - - - - - - - - - - - Webview top -1000 - - - - - - Webview top -1000 - - - - - - - - - - - Webview top -1000 - - - - - - Webview top -1000 - - - - - - - - - - - Webview top -1000 - - - - - - Webview top -1000 - - - - - - - - - - - - - - - - - - - - - Notebook List View - - - - - - Notebook List View - - - - - - - - - - - - - - - - - - - - - - - scrollTop 1050 - - - - - - scrollTop 1050 - - - - - - - - - - - Webview top -950 - - - - - - Webview top -950 - - - - - - - - - - - - - - - - - - - - - Notebook List View - - - - - - Notebook List View - - - - - - - - - - - - - - - - - - - - - - - scrollTop 1050 - - - - - - scrollTop 1050 - - - - - - - - - - - Webview top -950 - - - - - - Webview top -950 - - - - - - - - - - - - - Adjust top - - - - - - Adjust top - - - - - - - - - - - - - - - 1 - - - - - - 1 - - - - - - - - - - - 2 - - - - - - 2 - - - - - - - - - - - 3 - - - - - - 3 - - - - - - - - - - - 4 - - - - - - 4 - - - - - - - - - - - 4' - - - - - - 4' - - - - - - - - - - - 5 - - - - - - 5 - - - - - - - - - Viewer does not support full SVG 1.1 - - - - +Notebook List ViewNotebook List ViewWebview top -1000Webview top -1000ViewportViewportNotebook List ViewNotebook List ViewGrow Height by 50Grow Height by 50scrollTop 1000scrollTop 1000scrollTop 1000scrollTop 1000Notebook List ViewNotebook List ViewscrollTop 1050scrollTop 1050Notebook List ViewNotebook List ViewscrollTop 1050scrollTop 1050Adjust topAdjust topUpdateScrollTopUpdateScrollTopWebview top -1000Webview top -1000Webview top -1000Webview top -1000Webview top -1000Webview top -1000Notebook List ViewNotebook List ViewscrollTop 1050scrollTop 1050Webview top -950Webview top -950Notebook List ViewNotebook List ViewscrollTop 1050scrollTop 1050Webview top -950Webview top -950Adjust topAdjust top112233444'4'55Viewer does not support full SVG 1.1 \ No newline at end of file diff --git a/.github/instructions/resources/notebook/hybrid-find.drawio.svg b/.github/instructions/resources/notebook/hybrid-find.drawio.svg index a2419b58fce4e..7165dbff18ee5 100644 --- a/.github/instructions/resources/notebook/hybrid-find.drawio.svg +++ b/.github/instructions/resources/notebook/hybrid-find.drawio.svg @@ -1,327 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - window.find - - - - - - window.find - - - - - - - - - - - - - exec("hiliteColor") - - findMatchColor - - - - - - exec("hiliteColor")... - - - - - - - - - - - Serialize - - document.getSelection() - - - - - - Serialize... - - - - - - - - - - - - - - - - - Find in Rendered Outputs (Search in DOM) - - - - - - Find in Rendered Out... - - - - - - - - - - - Find - - - - - - Find - - - - - - - - - - - Find in Text Model - - - - - - Find in Text Model - - - - - - - - - - - Mix matches - - - - - - Mix matches - - - - - - - - - - - End of Doc - - - - - - End of Doc - - - - - - - - - - - - - Webview - - - - - - Webview - - - - - - - - - - - - - Find Next - - - - - - Find Next - - - - - - - - - - - - - Is Model Match - - - - - - Is Model Match - - - - - - - - - - - Reveal Editor - - - - - - Reveal Editor - - - - - - - - - - - Y - - - - - - Y - - - - - - - - - - - - - document create range - - - - - - document create range - - - - - - - - - - - - - "hiliteColor" - - currentFindMatchColor - - - - - - "hiliteColor"... - - - - - - - - - - - - - Find cell/output container - - - - - - Find cell/output con... - - - - - - - - - Viewer does not support full SVG 1.1 - - - - +window.findwindow.findexec("hiliteColor")findMatchColorexec("hiliteColor")...Serializedocument.getSelection()Serialize...Find in Rendered Outputs (Search in DOM)Find in Rendered Out...FindFindFind in Text ModelFind in Text ModelMix matchesMix matchesEnd of DocEnd of DocWebviewWebviewFind NextFind NextIs Model MatchIs Model MatchReveal EditorReveal EditorYYdocument create rangedocument create range"hiliteColor"currentFindMatchColor"hiliteColor"...Find cell/output containerFind cell/output con...Viewer does not support full SVG 1.1 \ No newline at end of file diff --git a/.github/instructions/resources/notebook/viewport-rendering.drawio.svg b/.github/instructions/resources/notebook/viewport-rendering.drawio.svg index 6a51b66c723b1..766736931fab9 100644 --- a/.github/instructions/resources/notebook/viewport-rendering.drawio.svg +++ b/.github/instructions/resources/notebook/viewport-rendering.drawio.svg @@ -1,521 +1 @@ - - - - - - - - - - - - - - Render Viewport - - - - - - Render Viewport - - - - - - - - - - - Notebook List View - - - - - - Notebook List View - - - - - - - - - - - - - - - - Render Template - - - - - - Render Template - - - - - - - - - - - - - - - - Render Element - - - - - - Render Element - - - - - - - - - - - - Get Dynamic Height - - - - - - Get Dynamic Height - - - - - - - - - - - - - - - - Create Cell Templates/Parts - - - - - - Create Cell Templates/Parts - - - - - - - - - - - Toolbar - - - - - - Toolbar - - - - - - - - - - - Editor - - - - - - Editor - - - - - - - - - - - Statusbar - - - - - - Statusbar - - - - - - - - - - - - - - Code Cell - - - - - - Code Cell - - - - - - - - - - - - Render Cell Parts - - - - - - Render Cell Parts - - - - - - - - - - - - - - - - - - CellPart read DOM - - - - - - CellPart read DOM - - - - - - - - - - - Update layout info - - - - - - Update layout info - - - - - - - - - - - - - - Toolbar.renderCell - - - - - - Toolbar.renderCell - - - - - - - - - - - Toolbar.renderCell - - - - - - Toolbar.renderCell - - - - - - - - - - - Toolbar.didRenderCell - - - - - - Toolbar.didRenderCell - - - - - - - - - - - Toolbar.renderCell - - - - - - Toolbar.renderCell - - - - - - - - - - - Toolbar.renderCell - - - - - - Toolbar.renderCell - - - - - - - - - - - - Toolbar.prepareLayout - - - - - - - Toolbar.prepareLay... - - - - - - - - - - - - - - - - - - Cell Layout Change - - - - - - Cell Layout Change - - - - - - - - - - - - Cell Part updateInternalLayoutNow - - - - - - Cell Part updateInternalLayoutNow - - - - - - - - - - - Toolbar.renderCell - - - - - - Toolbar.renderCell - - - - - - - - - - - Toolbar.renderCell - - - - - - Toolbar.renderCell - - - - - - - - - - - - Toolbar.updateInternalLayoutNow - - - - - - - Toolbar.updateInternalLayout... - - - - - - - - - - - Next Frame - - - - - - Next Frame - - - - - - - - - - - - DOM Read - - - - - - - DOM Read - - - - - - - - - - - - DOM Write - - - - - - - DOM Write - - - - - - - - - Viewer does not support full SVG 1.1 - - - - +Render ViewportRender ViewportNotebook List ViewNotebook List ViewRender TemplateRender TemplateRender ElementRender ElementGet Dynamic HeightGet Dynamic HeightCreate Cell Templates/PartsCreate Cell Templates/PartsToolbarToolbarEditorEditorStatusbarStatusbarCode CellCode CellRender Cell PartsRender Cell PartsCellPart read DOMCellPart read DOMUpdate layout infoUpdate layout infoToolbar.renderCellToolbar.renderCellToolbar.renderCellToolbar.renderCellToolbar.didRenderCellToolbar.didRenderCellToolbar.renderCellToolbar.renderCellToolbar.renderCellToolbar.renderCellToolbar.prepareLayoutToolbar.prepareLay...Cell Layout ChangeCell Layout ChangeCell Part updateInternalLayoutNowCell Part updateInternalLayoutNowToolbar.renderCellToolbar.renderCellToolbar.renderCellToolbar.renderCellToolbar.updateInternalLayoutNowToolbar.updateInternalLayout...Next FrameNext FrameDOM ReadDOM ReadDOM WriteDOM WriteViewer does not support full SVG 1.1 \ No newline at end of file diff --git a/.github/instructions/sessions.instructions.md b/.github/instructions/sessions.instructions.md index fe37147e5a494..b11f0fd54c440 100644 --- a/.github/instructions/sessions.instructions.md +++ b/.github/instructions/sessions.instructions.md @@ -59,8 +59,8 @@ Always use `Menus.*` from `browser/menus.ts` — never `MenuId.*` from `vs/platf All sessions-specific context keys live in `common/contextkeys.ts`: - `IsNewChatSessionContext` — whether showing the new session view -- `ActiveSessionProviderIdContext` — which provider owns the active session -- `ActiveSessionTypeContext` — session type of the active session +- `SessionProviderIdContext` — which provider owns the session in scope (the active session globally) +- `SessionTypeContext` — session type of the session in scope (the active session globally) - `IsPhoneLayoutContext` — whether in phone layout mode - `ChatBarVisibleContext` / `ChatBarFocusContext` — chat bar state diff --git a/.github/skills/add-policy/SKILL.md b/.github/skills/add-policy/SKILL.md index ef8c7a9f50f7c..50486d8ec4be5 100644 --- a/.github/skills/add-policy/SKILL.md +++ b/.github/skills/add-policy/SKILL.md @@ -25,8 +25,9 @@ Policies allow enterprise administrators to lock configuration settings via OS-l |--------|---------------|----------------------| | **OS-level** (Windows registry, macOS plist) | `NativePolicyService` via `@vscode/policy-watcher` | Watches `Software\Policies\Microsoft\{productName}` (Windows) or bundle identifier prefs (macOS) | | **Linux file** | `FilePolicyService` | Reads `/etc/vscode/policy.json` | -| **Account/GitHub** | `AccountPolicyService` | Reads `IPolicyData` from `IDefaultAccountService.policyData`, applies `value()` function. Server-delivered managed settings arrive on `policyData.managedSettings`; native MDM is a **separate** input (`ICopilotManagedSettingsService`) that `AccountPolicyService` selects between in `getPolicyData()` (server wins when present; no merging between layers) | -| **Copilot managed settings (native MDM)** | `CopilotManagedSettingsService` via `@vscode/policy-watcher` | Watches `SOFTWARE\Policies\GitHubCopilot` (Windows) / `com.github.copilot` prefs (macOS); feeds the canonical `managedSettings` bag — see [github-managed-settings.md](./github-managed-settings.md) | +| **Account/GitHub** | `AccountPolicyService` | Reads `IPolicyData` from `IDefaultAccountService.policyData`, applies `value()` function. Server-delivered managed settings arrive on `policyData.managedSettings`; native MDM (`INativeManagedSettingsService`) and a file on disk (`IFileManagedSettingsService`) are **separate** inputs that `AccountPolicyService` merges in `getPolicyData()` via `pickManagedSettings(nativeMdm, server, file)` (per-key precedence native MDM > server > file; a key locked by a higher channel cannot be overwritten, keys it leaves unset fall through to lower channels) | +| **Copilot managed settings (native MDM)** | `NativeManagedSettingsService` via `@vscode/policy-watcher` | Watches `SOFTWARE\Policies\GitHubCopilot` (Windows) / `com.github.copilot` prefs (macOS); feeds the canonical `managedSettings` bag — see [github-managed-settings.md](./github-managed-settings.md) | +| **Copilot managed settings (file)** | `FileManagedSettingsService` | Reads + watches `managed-settings.json` from a well-known per-OS path in the main process, exposed to renderers over IPC; lowest-precedence managed-settings channel — see [github-managed-settings.md](./github-managed-settings.md) | | **Multiplex** | `MultiplexPolicyService` | In the main process, combines multiple OS/file policy readers; in desktop and Agents-window renderers, combines the main-process `PolicyChannelClient` with `AccountPolicyService` | ### Key Files @@ -35,11 +36,12 @@ Policies allow enterprise administrators to lock configuration settings via OS-l |------|---------| | `src/vs/base/common/policy.ts` | `PolicyCategory` enum, `IPolicy` interface, `IPolicyReference`, `ManagedSettingsData`, `IManagedSettingsPolicyDefinitions` | | `src/vs/platform/policy/common/policy.ts` | `IPolicyService`, `AbstractPolicyService`, `PolicyDefinition`, `toSerializablePolicyDefinition` (drops the non-cloneable `value()` for IPC), `getRestrictedPolicyValue` | -| `src/vs/platform/policy/common/copilotManagedSettings.ts` | Managed-settings key constants, `collectManagedSettingsDefinitions`, `projectManagedSettings`, `flattenManagedSettings`, `ICopilotManagedSettingsService` | -| `src/vs/platform/policy/node/copilotManagedSettingsService.ts` | Native MDM watcher (`@vscode/policy-watcher`) for Copilot managed settings | +| `src/vs/platform/policy/common/copilotManagedSettings.ts` | Managed-settings key constants + well-known file paths, `collectManagedSettingsDefinitions`, `projectManagedSettings`, the shared `normalizeManagedSettings` (single normalizer for all channels), `pickManagedSettings` (per-key channel precedence), `INativeManagedSettingsService` / `IFileManagedSettingsService` | +| `src/vs/platform/policy/node/nativeManagedSettingsService.ts` | Native MDM watcher (`@vscode/policy-watcher`) for Copilot managed settings | +| `src/vs/platform/policy/common/fileManagedSettingsService.ts` | File-based channel: reads + watches `managed-settings.json` on a well-known per-OS path, normalizes via `normalizeManagedSettings` | | `src/vs/platform/configuration/common/configurations.ts` | `PolicyConfiguration` — bridges policies to configuration values; parses JSON-string managed settings back to typed values; applies values to `policyReference` settings | | `src/vs/platform/configuration/common/configurationRegistry.ts` | `policy` / `policyReference` registration; `getPolicyReferenceConfigurations()` (name → subordinate settings) | -| `src/vs/workbench/services/policies/common/accountPolicyService.ts` | Account/GitHub-based policy evaluation; selects + projects managed settings (server over MDM; single authoritative layer) | +| `src/vs/workbench/services/policies/common/accountPolicyService.ts` | Account/GitHub-based policy evaluation; selects + projects managed settings (native MDM over server; single authoritative layer) | | `src/vs/workbench/services/accounts/browser/managedSettings.ts` | `adaptManagedSettings` — normalizes the server `managed_settings` response into the canonical bag | | `src/vs/workbench/services/policies/common/multiplexPolicyService.ts` | Combines multiple policy services | | `src/vs/workbench/contrib/policyExport/electron-browser/policyExport.contribution.ts` | `--export-policy-data` CLI handler | @@ -326,4 +328,5 @@ Search the codebase for `policy:` to find all the examples of different policy c ## Learnings * Never hand-edit `build/lib/policies/policyData.jsonc` (its header explicitly forbids it). If `npm run export-policy-data` is failing, fix the script — don't patch the JSON. Common cause: running it in the wrong working directory (e.g. main repo instead of a worktree), which silently exports the wrong source tree. -* Document **behavior and business-logic expectations**, not copy-pasted implementation. Reproducing internal code (e.g. the `getPolicyData()` merge body) in the skill rots the moment the source changes and adds no information beyond the source itself. State the contract in prose (e.g. "server-delivered managed settings win over native MDM; the two layers are never merged") and point to the source for the implementation. Reserve code blocks for the **author-facing API contract** a contributor must follow — how to *declare* a `policy` / `managedSettings` / `value` callback — not for restating runtime plumbing. +* **Regenerate `policyData.jsonc` in a clean environment, or the `PolicyExport` integration test will fail in CI.** `referencedSettings` is only captured for references **loaded at export time**. A plain `npm run export-policy-data` loads your **dev-profile extensions** (e.g. the Copilot extension), which injects `referencedSettings` onto core policies that the test's **fixture-based** export (clean profile, no user extensions) won't produce — so the checked-in file ends up with extra `referencedSettings` and CI fails. This is **not reproducible locally** because the test reuses your default extensions dir. Regenerate the way the test exports: `DISTRO_PRODUCT_JSON= ./scripts/code.sh --export-policy-data="$PWD/build/lib/policies/policyData.jsonc" --user-data-dir="$(mktemp -d)" --extensions-dir="$(mktemp -d)"` (with `VSCODE_SKIP_PRELAUNCH=1`). +* Document **behavior and business-logic expectations**, not copy-pasted implementation. Reproducing internal code (e.g. the `getPolicyData()` merge body) in the skill rots the moment the source changes and adds no information beyond the source itself. State the contract in prose (e.g. "native MDM managed settings win over the server-delivered channel; the two layers are never merged") and point to the source for the implementation. Reserve code blocks for the **author-facing API contract** a contributor must follow — how to *declare* a `policy` / `managedSettings` / `value` callback — not for restating runtime plumbing. diff --git a/.github/skills/add-policy/github-managed-settings.md b/.github/skills/add-policy/github-managed-settings.md index 41f34e50245bc..8f6bb41039761 100644 --- a/.github/skills/add-policy/github-managed-settings.md +++ b/.github/skills/add-policy/github-managed-settings.md @@ -9,7 +9,7 @@ Managed settings layer **on top of** the existing policy framework. They do **no introduce a new `IPolicyService`; they feed `IPolicyData.managedSettings`, which the existing `policy.value(policyData)` callback already consumes via `AccountPolicyService`. -## The big idea: one canonical bag, two delivery channels (in VS Code) +## The big idea: one canonical bag, three delivery channels (in VS Code) Every enterprise-managed Copilot setting resolves through a single normalized bag: @@ -41,26 +41,27 @@ and parsed back into the object-typed setting on read by `PolicyConfiguration`. ### Delivery channels -VS Code implements **two** channels feeding the bag; the external schema additionally -describes a file-based channel that other Copilot clients may implement but that VS Code -does **not** read today (no `managed-settings.json` reader exists in `src/`). +VS Code implements **three** channels feeding the bag, matching the external schema's +described delivery slots (native MDM, server-managed, and file-based). | Channel | Where it's read | Implementation | Lands on | |---------|-----------------|----------------|----------| -| **Native MDM** (Windows registry / macOS plist) | OS managed preferences | `CopilotManagedSettingsService` (`src/vs/platform/policy/node/copilotManagedSettingsService.ts`) via `@vscode/policy-watcher` | `ICopilotManagedSettingsService.managedSettings` | +| **Native MDM** (Windows registry / macOS plist) | OS managed preferences | `NativeManagedSettingsService` (`src/vs/platform/policy/node/nativeManagedSettingsService.ts`) via `@vscode/policy-watcher` | `INativeManagedSettingsService.managedSettings` | | **Server-managed** (`/copilot_internal/managed_settings`) | GitHub endpoint; per the code comment in `managedSettings.ts`, it returns the enterprise's `.github/copilot/settings.json` content | `adaptManagedSettings` (`src/vs/workbench/services/accounts/browser/managedSettings.ts`) → `DefaultAccountService.policyData` | `accountPolicyData.managedSettings` | -| **File-based** (`managed-settings.json`) | external schema only | *not implemented in VS Code* | — | +| **File-based** (`managed-settings.json`) | well-known per-OS disk path (e.g. `/Library/Application Support/GitHubCopilot/` on macOS), read in the main process and exposed to renderer windows over IPC | `FileManagedSettingsService` (`src/vs/platform/policy/common/fileManagedSettingsService.ts`) | `IFileManagedSettingsService.managedSettings` | -Both VS Code channels converge in `AccountPolicyService.getPolicyData()`. +All three VS Code channels converge in `AccountPolicyService.getPolicyData()`. -**Precedence: server-delivered managed settings win over native MDM.** There is a -single authoritative source at any point in time — the two layers are **not** merged. -When the server delivers managed settings, native MDM (`nativeManagedSettings`) is -ignored entirely; native MDM applies only when the server provides no managed settings. -Rationale: the server is harder to bypass than local MDM/file policies, and admins need -one authoritative source to reason about. The winning layer is then projected onto the -declared schema (see below). Client-side merging still happens *within* the winning -layer (e.g. `enabledPlugins`, `extraKnownMarketplaces`). +**Precedence: native MDM managed settings win over the server-delivered channel, which in +turn wins over the file-based channel** (`pickManagedSettings` in `copilotManagedSettings.ts`). +Precedence is resolved **per key**: for each key the highest-precedence channel that supplies +it wins, but a key that the higher channels leave unset is still filled in by a lower channel. +A value an admin locks via native MDM therefore cannot be overwritten by the server or a file, +while keys the higher channels never set remain available to lower ones. Rationale for the +order: the server is harder to bypass than local MDM, and a local file is the most easily +tampered with. The merged bag is then projected onto the declared schema (see below). +Client-side merging still happens *within* a channel's value (e.g. `enabledPlugins`, +`extraKnownMarketplaces`). ## Schema source of truth @@ -196,25 +197,27 @@ delivery slot for the managed value. | Function | Role | |----------|------| -| `flattenManagedSettings(obj)` | Flattens a nested response into dot-path scalars (used by the server adapter). | +| `flattenManagedSettings(obj)` | Flattens a nested response into dot-path scalars (used by `normalizeManagedSettings`). | +| `normalizeManagedSettings(parsed, onWarn?)` | The **single** normalizer shared by every channel: turns a parsed managed-settings object (server response, file on disk, …) into the canonical bag. Scalar leaves flatten; structured keys (declared in the `STRUCTURED_MANAGED_SETTINGS` table) are JSON-encoded under a single key each. The server adapter `adaptManagedSettings` and the file channel both call it. | | `collectManagedSettingsDefinitions(policyDefinitions)` | Aggregates every policy's `managedSettings` into one `key → { type }` map. **Single source of truth** for which keys (and types) are honored; drives both the MDM watcher and the server projection. | | `hasManagedSettingsDefinitions(policyDefinitions)` | Cheap short-circuiting existence check (does *any* policy declare a managed key?) — used to decide whether the native MDM watcher is needed at all, without building the full aggregate. | | `projectManagedSettings(values, definitions, onWarn?)` | Keeps only declared keys whose runtime value **matches the declared type**. Undeclared keys and type mismatches are **dropped (validated, never coerced)**, with an optional warning. | +| `pickManagedSettings(nativeMdm, server, file)` | Merges the channels **per key** by precedence (native MDM → server → file): the highest-precedence channel that sets a key wins, lower channels fill in keys the higher ones leave unset, and every contribution is recorded for provenance. **The extension point when adding a new channel** — extend the `ManagedSettingsChannel` union, the `MANAGED_SETTINGS_CHANNELS` order, and this function together. | | `managedSettingValue(key)` | Builds the standard pass-through `value` callback `policyData => policyData.managedSettings?.[key]`. Use for the common "lock to the managed value, else fall through" case (see [Declaring a managed setting](#declaring-a-managed-setting-on-a-policy)). | -### Server-side adaptation: the structured-key descriptor table +### Normalization: the structured-key descriptor table -`adaptManagedSettings` (`managedSettings.ts`) normalizes the server `managed_settings` -response into the canonical bag. Scalar leaves flatten directly; **structured** (object/array) +`normalizeManagedSettings` (`copilotManagedSettings.ts`) turns a parsed managed-settings +object into the canonical bag, and is shared by every channel — the server adapter +`adaptManagedSettings` (`managedSettings.ts`) is a thin wrapper around it, and the file +channel calls it directly. Scalar leaves flatten directly; **structured** (object/array) keys are JSON-encoded under a single bag key. The per-key knowledge for the structured ones -lives in **one table**, `STRUCTURED_MANAGED_SETTINGS`. Each row declares the canonical bag -`key`, the `responseField` it consumes (a hand-maintained union kept in sync with -`IManagedSettingsResponse`'s named fields — not compiler-enforced, because the response's index -signature widens `keyof` to `string`; the `adaptManagedSettings` tests are the drift backstop), -and an `encode(raw, onWarn?)` that +lives in **one table**, `STRUCTURED_MANAGED_SETTINGS`. Each row declares the `key` (the source +field name read from the input, which is also the canonical bag key the JSON string is stored +under — identical for structured settings by contract) and an `encode(raw, onWarn?)` that normalizes the value before `JSON.stringify`. Adding a structured key is **one descriptor row** -(plus its response-interface field and the policy declaration) — no new bespoke branch in the -adapter. +(plus the server `IManagedSettingsResponse` field and the policy declaration) — no new bespoke branch in the +normalizer. The `adaptManagedSettings` / `normalizeManagedSettings` tests are the drift backstop. Constants (also in `copilotManagedSettings.ts`): @@ -232,33 +235,41 @@ Constants (also in `copilotManagedSettings.ts`): Native MDM is desktop-main only (`src/vs/code/electron-main/main.ts`). The real wiring constructs the platform service first (Windows / macOS only), then registers it — falling -back to `NullCopilotManagedSettingsService` on Linux (abbreviated): +back to `NullNativeManagedSettingsService` on Linux (abbreviated): ```ts -let copilotManagedSettingsService: CopilotManagedSettingsService | undefined; +let nativeManagedSettingsService: NativeManagedSettingsService | undefined; if (isWindows) { - copilotManagedSettingsService = new CopilotManagedSettingsService( + nativeManagedSettingsService = new NativeManagedSettingsService( logService, GITHUB_COPILOT_WIN32_POLICY_NAME, { registryPath: GITHUB_COPILOT_WIN32_REGISTRY_PATH }); } else if (isMacintosh) { - copilotManagedSettingsService = new CopilotManagedSettingsService( + nativeManagedSettingsService = new NativeManagedSettingsService( logService, GITHUB_COPILOT_MACOS_BUNDLE_ID); } -if (copilotManagedSettingsService) { - services.set(ICopilotManagedSettingsService, copilotManagedSettingsService); +if (nativeManagedSettingsService) { + services.set(INativeManagedSettingsService, nativeManagedSettingsService); } else { - services.set(ICopilotManagedSettingsService, new NullCopilotManagedSettingsService()); + services.set(INativeManagedSettingsService, new NullNativeManagedSettingsService()); } ``` -It is exposed to the renderer over IPC via `CopilotManagedSettingsChannel` / -`CopilotManagedSettingsChannelClient` (`copilotManagedSettingsIpc.ts`), registered as -the `copilotManagedSettings` channel in `app.ts`. `AccountPolicyService` subscribes to +It is exposed to the renderer over IPC via `NativeManagedSettingsChannel` / +`NativeManagedSettingsChannelClient` (`nativeManagedSettingsIpc.ts`), registered as +the `nativeManagedSettings` channel in `app.ts`. `AccountPolicyService` subscribes to `onDidChangeManagedSettings` and re-evaluates policy values when managed settings change. The service only watches keys that some policy declares: `updatePolicyDefinitions` calls `collectManagedSettingsDefinitions`, then `@vscode/policy-watcher` watches exactly those dot-paths. No declared keys ⇒ no watcher. +The **file-based** channel is wired the same way (`src/vs/code/electron-main/main.ts`): +`FileManagedSettingsService` reads `managed-settings.json` from the per-OS well-known path +(`MANAGED_SETTINGS_*` constants in `copilotManagedSettings.ts`), falling back to +`NullFileManagedSettingsService` when no path applies. It is exposed to the renderer over IPC +via `FileManagedSettingsChannel` / `FileManagedSettingsChannelClient` +(`fileManagedSettingsIpc.ts`), registered as the `fileManagedSettings` channel in `app.ts`, +and `AccountPolicyService` subscribes to its `onDidChangeManagedSettings` too. + ## Adding a brand-new managed-settings key (checklist) 1. **Pick the canonical dot-path** and add it as a constant in @@ -268,11 +279,12 @@ those dot-paths. No declared keys ⇒ no watcher. and a `value` callback. For a plain pass-through use `value: managedSettingValue(KEY)`; only hand-write the callback when combining with another condition. 3. **If the value is structured** (object/array), declare the managed key as `'string'` and - let `PolicyConfiguration` parse the JSON back on read. Then teach the **server adapter** by - adding one row to `STRUCTURED_MANAGED_SETTINGS` in `managedSettings.ts` (canonical `key`, the - `responseField` it consumes, and an `encode` that normalizes the server shape into what an - admin would author in MDM) plus the matching field on `IManagedSettingsResponse`. That table - is the single place server-side structured-key handling lives. + let `PolicyConfiguration` parse the JSON back on read. Then teach the **shared normalizer** by + adding one row to `STRUCTURED_MANAGED_SETTINGS` in `copilotManagedSettings.ts` (the `key` and an + `encode` that normalizes the raw shape into what an admin would author in MDM) plus the matching + field on the server `IManagedSettingsResponse`. That table + is the single place structured-key handling lives, and it applies to every channel (server, + file-based, …) because they all funnel through `normalizeManagedSettings`. 4. **Keep the schema aligned.** The runtime, the server endpoint, and `managed-settings-schema.json` must agree on the key name and value type. The declaration-driven projection (`projectManagedSettings`) silently drops anything that @@ -282,11 +294,13 @@ those dot-paths. No declared keys ⇒ no watcher. Reference tests: - `src/vs/platform/policy/test/common/copilotManagedSettings.test.ts` -- `src/vs/platform/policy/test/node/copilotManagedSettingsService.test.ts` +- `src/vs/platform/policy/test/node/nativeManagedSettingsService.test.ts` - `src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts` - `src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts` (includes an end-to-end equivalence test: a server JSON string and a native MDM JSON string resolve to the **identical** typed object). +- `src/vs/platform/policy/test/common/fileManagedSettingsService.test.ts` + (covers `normalizeManagedSettings` and the file-based channel reader). **Manual/local testing:** use the mock policy server to serve arbitrary `managed_settings` (and entitlement/token) responses and apply them via @@ -342,6 +356,6 @@ Rules & internals: | PR | Change | |----|--------| | [#318623](https://github.com/microsoft/vscode/pull/318623) | Wire `/copilot_internal/managed_settings` into `AccountPolicyService`/`IPolicyData`; add `chat.plugins.enabledPlugins`/`extraMarketplaces`/`strictMarketplaces` settings + `adaptManagedSettings` shape adaptation. No new `IPolicyService`. | -| [#320991](https://github.com/microsoft/vscode/pull/320991) | Add native MDM delivery: `CopilotManagedSettingsService` + `@vscode/policy-watcher`; let policies declare `managedSettings` mappings; wire the first V0 key `permissions.disableBypassPermissionsMode` → force `ChatToolsAutoApprove=false`. | +| [#320991](https://github.com/microsoft/vscode/pull/320991) | Add native MDM delivery: `NativeManagedSettingsService` + `@vscode/policy-watcher`; let policies declare `managedSettings` mappings; wire the first V0 key `permissions.disableBypassPermissionsMode` → force `ChatToolsAutoApprove=false`. | | [#321218](https://github.com/microsoft/vscode/pull/321218) | Make `IPolicyData.managedSettings` the **single** channel: server + native MDM project into one canonical bag; structured settings carried as canonical JSON strings; remove the typed `enabledPlugins`/`extraKnownMarketplaces`/`strictKnownMarketplaces` fields. End-to-end equivalence test. | | [#321515](https://github.com/microsoft/vscode/pull/321515) | Add `policyReference` so one policy governs many settings; callback-free serialization; catalog + diagnostics list governed settings. Used to gate Claude (`Claude3PIntegration`) and Codex (`Codex3PIntegration`) across the editor and Agents windows. | diff --git a/.github/skills/azure-pipelines/SKILL.md b/.github/skills/azure-pipelines/SKILL.md index 8903496983c88..2e7b7f15ec7c6 100644 --- a/.github/skills/azure-pipelines/SKILL.md +++ b/.github/skills/azure-pipelines/SKILL.md @@ -119,6 +119,7 @@ node .github/skills/azure-pipelines/azure-pipeline.ts queue --parameter "VSCODE_ | `VSCODE_PUBLISH` | boolean | `true` | `true`, `false` | Publish to builds.code.visualstudio.com | | `VSCODE_RELEASE` | boolean | `false` | `true`, `false` | Trigger release flow if successful | | `VSCODE_STEP_ON_IT` | boolean | `false` | `true`, `false` | Skip tests | +| `VSCODE_USE_LEGACY_OSS_NOTICE` | boolean | `false` | `true`, `false` | Keep the legacy mixin ThirdPartyNotices.txt instead of the Component Governance notice | Example: run a quick CI-oriented validation with minimal publish/release side effects: diff --git a/.github/skills/chat-perf/SKILL.md b/.github/skills/chat-perf/SKILL.md index 7769d511cf41c..41c5e22524a4b 100644 --- a/.github/skills/chat-perf/SKILL.md +++ b/.github/skills/chat-perf/SKILL.md @@ -42,6 +42,8 @@ npm run perf:chat-leak -- --messages 20 --verbose Launches VS Code via Playwright Electron, opens the chat panel, sends a message with a mock LLM response, and measures timing, layout, and rendering metrics. By default, downloads VS Code 1.115.0 as a baseline, benchmarks it, then benchmarks the local dev build and compares. +> **You don't always need a baseline.** A baseline exists only for *comparison* (regression detection). If you just want the current build's numbers — profiling a single change, capturing traces/heap snapshots, or iterating on a scenario — pass `--no-baseline` to skip downloading and benchmarking the baseline entirely (roughly halves runtime). Baseline comparison is what turns raw measurements into a pass/fail verdict; without it you still get all the metrics, just no verdict. + ### Key flags | Flag | Default | Description | @@ -51,7 +53,7 @@ Launches VS Code via Playwright Electron, opens the chat panel, sends a message | `--build ` / `-b` | local dev | Build to test. Accepts path or version (`1.110.0`, `insiders`, commit hash). | | `--baseline ` | — | Compare against a previously saved baseline JSON file. | | `--baseline-build ` | `1.115.0` | Version or local path to benchmark as baseline. | -| `--no-baseline` | — | Skip baseline comparison entirely. | +| `--no-baseline` | — | Skip the baseline entirely — just measure the test build (no download, no comparison, ~2× faster). Use when you only need raw numbers, not a regression verdict. | | `--save-baseline` | — | Save results as the new baseline (requires `--baseline `). | | `--resume ` | — | Resume a previous run, adding more iterations to increase confidence. | | `--threshold ` | `0.2` | Regression threshold (0.2 = flag if 20% slower). | @@ -60,6 +62,7 @@ Launches VS Code via Playwright Electron, opens the chat panel, sends a message | `--force` | — | Skip build mode mismatch confirmation prompt. | | `--ci` | — | CI mode: write Markdown summary to `ci-summary.md` (implies `--no-cache`, `--heap-snapshots`, `--cleanup-diagnostics`). | | `--heap-snapshots` | — | Take heap snapshots after each run (slow; auto-enabled in `--ci` mode). | +| `--gc-object-stats` | — | **GC deep-dives only.** Enables V8 `gc_stats` tracing (per-type heap object dump on every GC). ⚠️ Corrupts all timing metrics — a major GC landing mid-request adds ~550ms — so never use it for benchmarking. Off by default; prefer heap snapshots for memory analysis. | | `--cleanup-diagnostics` | — | Delete heap snapshots, CPU profiles, and traces to save disk. During runs, only the latest run's files are kept; after comparison, files for non-regressed scenarios are deleted. Auto-enabled in `--ci` mode. | | `--setting ` | — | Set a VS Code setting override for all builds (repeatable). | | `--test-setting ` | — | Set a VS Code setting override for the test build only. | @@ -186,13 +189,13 @@ Run `npm run perf:chat -- --help` to see the full list of registered scenario ID Only these metrics trigger a regression failure (when they exceed the threshold with statistical significance): - `timeToFirstToken`, `timeToComplete` — user-perceived latency +- `layoutDurationMs` — total layout time from the trace (the *real* layout cost) - `forcedReflowCount` — forced synchronous layouts are always bad - `longTaskCount`, `longAnimationFrameCount` — main thread jank These are reported but **informational only** (won't fail CI): -- `layoutCount` — inflated by CSS animations; use `layoutDurationMs` instead -- `layoutDurationMs` — total layout time from trace (more meaningful than count) -- `recalcStyleCount` — inflated by CSS animations (compositor-driven, cheap) +- `layoutCount` — number of layout ops; inflated by CSS animations (compositor-driven, cheap). A build can do *more but cheaper* layouts, so gate on `layoutDurationMs`, not this count. +- `recalcStyleCount` — number of style recalcs; inflated by CSS animations (compositor-driven, cheap) - `timeToRenderComplete` — includes typewriter animation tail - Memory/heap metrics — too noisy for single-request benchmarks @@ -229,6 +232,54 @@ Launches one VS Code session, sends N messages sequentially, forces GC between e - DOM nodes stable after first message — normal (chat list virtualization working) - DOM nodes growing linearly — rendering leak, check disposable cleanup +## CI runs & pinpointing regressions + +The perf + leak checks run in CI via the **`.github/workflows/chat-perf.yml`** workflow (a scheduled daily `workflow_dispatch`, plus manual dispatch). Each run benchmarks the current `main` as the **test** build against a fixed release **baseline** (from `config.jsonc`, e.g. `1.122.0`). Because the baseline is fixed, the **test** median for a metric across successive daily runs traces `main`'s trajectory over time — that's what lets you bisect *when* something regressed or went flaky. + +### Finding and reading historic runs + +```bash +# List recent runs (most recent first) — note the run IDs and dates +gh run list --workflow chat-perf.yml -R microsoft/vscode --limit 30 \ + --json databaseId,status,conclusion,createdAt,headBranch \ + --jq '.[] | [.databaseId, (.conclusion//.status), .createdAt, .headBranch] | @tsv' + +# See a run's per-job results +gh run view -R microsoft/vscode --json jobs \ + --jq '.jobs[] | [.name, (.conclusion//.status)] | @tsv' + +# Trigger a run manually against any ref/version: +gh workflow run chat-perf.yml -R microsoft/vscode --ref main \ + -f test_build= -f baseline_build= +``` + +### Artifacts per run (and retention — this matters for old runs) + +| Artifact | Contents | Retention | +|---|---|---| +| `chat-perf-summary` | Unified `ci-summary.md`: verdicts, per-metric medians ±stddev, **per-run raw tables** | 30 days | +| `perf-results-` | Everything below **plus traces, CPU profiles, heap snapshots** (large) | 30 days | +| `leak-results` | Leak log + `chat-simulation-leak-results.json` | 30 days | +| `perf-summary-` | `results.json` (full per-run metrics incl. `rawRuns`) + `baseline-*.json` | **1 day** | + +```bash +# List a run's artifacts + whether they've expired +gh api repos/microsoft/vscode/actions/runs//artifacts \ + --jq '.artifacts[] | [.name, .expired] | @tsv' + +# Download the human-readable summary (best first stop; survives 30 days) +gh run download -R microsoft/vscode -n chat-perf-summary +``` + +### Pinpointing where a metric regressed / went flaky + +1. **Bisect across dates.** Pull `chat-perf-summary/ci-summary.md` from several runs spanning the window. Compare the **test** median (and ±stddev) for the suspect metric+scenario run-to-run — the day it jumps is when `main` changed. A metric that goes *bimodal* (e.g. a raw-run column showing two clusters like `~250 / ~900`) is the flaky signature; a stable shift is a genuine regression. +2. **Confirm it's real vs. measurement noise.** Check the per-run raw tables (in `ci-summary.md`) — high ±stddev / bimodal values mean the *median* is being pulled around by a few outlier runs, not a uniform slowdown. +3. **Deep-dive the cause.** Download `perf-results-` (has the `trace.json` per run) for a slow run and inspect what dominates the slow window. Trick that found the `gc_stats` artifact: sum main-thread `RunTask` durations between two `code/chat/*` marks (e.g. `willCollectInstructions` → `didCollectInstructions`) — if the window is ~0% busy, it's an async wait or a GC pause, not real work; then look at the largest `X`-phase events in that window (`MajorGC`, `Layout`, etc.). +4. **Reproduce a suspect commit locally** to bisect precisely: `npm run perf:chat -- --build --baseline-build --runs 7` (or two commits directly via `--build --baseline-build `). + +> Tip: `perf-summary-*` (the machine-readable `results.json` with `rawRuns`) is deleted after **1 day**, so for older runs rely on `chat-perf-summary` (raw tables, 30 days) or extract `results.json` from `perf-results-*` (also 30 days). + ## Architecture ``` diff --git a/.github/skills/integrated-browser/SKILL.md b/.github/skills/integrated-browser/SKILL.md new file mode 100644 index 0000000000000..c4ace6680938b --- /dev/null +++ b/.github/skills/integrated-browser/SKILL.md @@ -0,0 +1,87 @@ +--- +name: integrated-browser +description: Use this when working on the VS Code integrated browser ("browserView") to understand its architecture and mental model. Covers the embedded Chromium browser, its editor tab, navigation, overlay/layout, sessions, and agent browser tools under `src/vs/platform/browserView` and `src/vs/workbench/contrib/browserView`. +--- + +# Integrated Browser Architecture + +The integrated browser ("browserView") embeds a **real Chromium browser** in VS Code, backed by an Electron `WebContentsView`. It renders live pages, presents each as an editor tab, and lets agents drive those pages through tools. It powers the in-product browser tab and the agent "browser" tools. It is **not** the old `extensions/simple-browser` (an iframe-in-a-webview), which now delegates to this on desktop. + +It's a heavyweight, security-sensitive, multi-process primitive, and **almost every design decision follows from that.** This file describes the load-bearing ideas that rarely change. It deliberately does **not** enumerate current features/tools/commands/settings — those churn; the live `features/` and `tools/` folders are the source of truth. Build the mental model here, then go read the specific code you're changing. + +## The one idea everything follows from + +**A page is a native `WebContentsView` that only the main process may create, own, and position. Nothing else can touch it directly.** It's owned by the main process and painted by the OS compositor *on top of* the workbench DOM — not inside it. Everything else works around this: + +- The **renderer** (editor UI + agent tools) can't hold the page; it holds a model/proxy and talks to main over IPC. +- **Playwright** is heavy and long-lived, so it runs in the **shared process**, reaching the page over IPC too. +- The page **paints over the DOM**, so the workbench choreographs alignment, z-order, focus, and screenshots by hand. + +## Three processes, and why + +| Process | Location | What lives here | +|---------|----------|-----------------| +| **Main** | `platform/browserView/electron-main` | The `WebContentsView`, sessions, trust, permissions, history, CDP, screenshots — authoritative page state. Only main can create native views. | +| **Shared** | `platform/browserView/node` | Playwright + remote/group automation services. Keeps a heavy dependency out of main (stability) and renderer (lifecycle). | +| **Renderer** | `workbench/contrib/browserView` + `platform/browserView/electron-browser` | Editor pane, UI feature contributions, agent tools, page preload script. Holds only lightweight proxies. | + +**Layer rule:** `platform` must not import `workbench`; the agent host can't import `workbench`; shared types belong in `platform/common`. The renderer reaches main/shared only through `ProxyChannel` IPC, never by importing implementations. Channels are registered in `electron-main/app.ts` and `electron-utility/sharedProcess/sharedProcessMain.ts`. Split: **page ops/state → main; agent automation → shared; CDP plumbing is its own channel both renderer and shared use to reach main.** + +## The renderer holds a mirror, not the truth + +The renderer model is a **read replica** of state owned by the main-process view. Flow is one-directional: + +``` +renderer feature ──command──▶ model ──IPC──▶ main BrowserView ──▶ Chromium + ▲ │ + └──────────── event (state changed) ◀──────────┘ +``` + +To *do* something, call a method (round-trips to main); to *react*, listen to a model event. Never **compute** page state in the renderer — it has no access to the web contents. + +- **New state** (url/title/loading/zoom/…): make it authoritative in the main `BrowserView`, emit a change event, then mirror the field + event on the renderer model and forward it over the channel. +- **New operation** (navigate/reload/focus/…): define it in main, expose a thin proxy method on the renderer model that round-trips the channel. + +Wanting the renderer to "just read" something off the page is the signal you need new mirrored state + an event from main. + +## The native view floats above the DOM (the overlay problem) + +The single most error-prone area — the cause of nearly every "won't move / misaligned / shows through a menu / won't focus" bug. The page is painted by main in **screen coordinates on top of** the renderer, so the workbench fakes a normal DOM element. Treat the page's rectangle, visibility, focus, and imagery as things the workbench **coordinates**, never DOM facts: + +- **Alignment.** The editor renders an empty DOM stub, measures its screen rect, and ships bounds to main. CSS zoom and screen pixels disagree, so bounds are **pixel-snapped**. New layout that moves the page must feed this bounds computation, not just move a DOM box. +- **Z-order.** The native view paints *above* all workbench UI, so menus/popups/hovers/dialogs that should sit over the page would be hidden. The workbench detects overlap and **hides the native view**, swapping in a placeholder. New floating UI over the page must be detectable by that machinery — a high CSS z-index won't do it. +- **Flicker masking.** While hidden/repositioning, a periodic screenshot stands in. Screenshots are also the only legit way page imagery reaches the renderer/agents — nobody reads native pixels. +- **Focus & keyboard.** Focus is bridged explicitly. A **preload script** (injected into every page in an isolated world) decides which keystrokes the page keeps vs forwards to VS Code keybindings. Treat it as a **trust boundary**: assume a hostile page, keep it minimal and side-effect-free. + +## A browser tab is a real editor, extended by contributions + +A page is a normal editor — a read-only, serializable `EditorInput` + `EditorPane` on the standard registry, resolving lazily to a view-model (unloadable without closing the tab). This inherits tabs, splitting, persistence, focus, and keybinding scoping for free. + +The pane is thin. Behavior is added via a **local contribution model (separate from workbench contributions)**: small classes that attach to the editor, get DI, and hook a fixed lifecycle (model attach/detach, layout overrides, resize/visibility, focus, UI insertion). Each gets a lifetime **scoped to the attached model**, so per-page disposables clean up on navigate/close. Even native-view rendering is just one contribution. + +**To add behavior, write a new contribution modeled on a sibling — don't grow the editor or the main view.** Contributions affecting the page rect compose through **prioritized layout overrides** (lower runs first; e.g. emulation sizes the viewport, pixel-snap runs last), so **order matters** — never hard-code pixels. + +## Identity and isolation: sessions, groups, CDP + +Keep these three distinct: + +- **Session = storage identity.** Each Electron session maps 1:1 to a `BrowserSession` (cookies, cache, storage), and its id **doubles as the CDP browser-context id**. Scoped **global**, **per-workspace**, or **ephemeral**; multiple tabs can share one. Security is enforced here: `file://`, certificate trust, and permissions are all gated at the session (e.g. local files need workspace trust). New capabilities that expand a page's reach belong here, not on a feature. +- **Group = automation visibility.** A group assembles a dynamic set of views and exposes them as one logical CDP "browser." Groups *reference* views without owning them; a view can be in several. This is how different clients (chat session, DevTools, an extension) each see only their subset. +- **CDP is proxied, never raw.** A protocol-aware proxy implements browser/target-level domains (discovery, auto-attach, flattened sessions, contexts) and forwards the rest per-target. This lets one logical browser be stitched from views that come and go, and lets Playwright connect without touching Chromium directly. + +Cookies/login/storage → **sessions**. "Which pages can this client see" → **groups**. The protocol itself → **the proxy**. + +## Agents share the user's page — under a privacy gate + +- **Same page, shared cooperatively.** Playwright drives the *same* `WebContentsView` the user sees, via CDP, one connection per chat session. Human and agent actions can collide; conflicts resolve in the user's favor (a human prompt/dialog can interrupt automation). The workbench (not Playwright) owns device emulation, so Playwright's auto-emulation is suppressed except during an agent action. Assume a human may interact with the same page concurrently. +- **Pages are private until shared.** Content isn't visible to agents by default. A page's *sharing state* gates content; a separate **availability gate** (chat enabled, agent mode, settings) decides whether the full tool set is registered — when it isn't, only a reduced "open a URL without content access" capability exists. URLs are screened by the network-filter and masked when blocked. Treat page content as **untrusted model input** (prompt injection). Any new agent surface must honor these gates. (Tool *names* live in `platform` so the agent host, which can't depend on `workbench`, can reference them.) + +## Remote pages + +When a page must load as if from a remote machine (forwarded `localhost` in a remote workspace, container, or Codespace), a **tunnel proxy is applied to the page's session**; credentials come from the extension host, and navigation can defer until the proxy is live. "Open localhost" isn't always local — remote URLs are rewritten to their forwarded form, and the proxy lives on the **session**, not an individual call. + +## Practical guidance + +- **Desktop-only.** Nothing runs in web; add to `electron-*` / `node` and let the `browser/` stubs throw "not available in web". +- **Match existing patterns.** New capability → a new feature contribution and/or tool, modeled on a sibling. Cross-process state → a model method + event from main, not local renderer state. Layout change → a prioritized override, not pixels. +- **Mind the trust boundaries:** the preload (hostile page), the session (storage / permissions / file access), agent gating (sharing + availability + network filter), and chat attachment (prompt injection). diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index e02331b1305c3..57d9aefef5d28 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -34,21 +34,47 @@ Then read the relevant spec for the area you are changing (see table below). If - **Modifying workbench code**: Prefer extending/wrapping workbench classes in the sessions layer over modifying shared workbench components. - **Timeouts as fixes**: Never use `setTimeout`/`disposableTimeout`/arbitrary delays to fix bugs or implement behaviour. They are race-prone guesses that mask the real ordering/state problem. Drive logic off deterministic signals instead — observables (`autorun`/`derived`), explicit events (`onDidChange*`), lifecycle phases, or awaiting the actual async operation. - **Stashed state read back later (side-channels)**: Never stash a value on a service during one method call and read it back from a separate query later, assuming it is still valid (e.g. a `Set`/flag set in `openSession` and consumed by a `shouldX()` pull-API). This is fragile temporal coupling. Instead, make it reactive state that is set **atomically together with its source of truth** and consumed reactively. Example: per-activation intent like "open in background / preserve focus" is exposed as an `IObservable` set in the **same transaction** as `activeSession` (via a single internal setter so it can never go stale), and read with `.read(reader)` in the consumer's `autorun` — never via a consume-once getter. +- **Provider-owned model/mode selection belongs in the loaded chat model, with draft persistence driven by debounce**: For AHP-backed chats, `setModel` / `setAgent` must push the selection into the loaded `IChatModel.inputModel` (like `_updateChatSessionState`) and let the draft-sync debounce emit `chat/draftChanged`. Do not immediately dispatch a model/agent-only draft from the provider, because it can overwrite unsaved typed text before the debounced full input-state draft is persisted. - **Blocking on a "pending/waiting" state instead of creating + upgrading**: When an entity (e.g. a draft session) depends on something that registers asynchronously, don't withhold creation behind a pending/waiting state. Prefer creating immediately with the best available data, then **replace/upgrade** it once the awaited dependency arrives (driven by an `onDidChange*`/observable signal), cancelling the upgrade if the user changes the inputs meanwhile. Do **not** bound the upgrade with a timeout or even a lifecycle milestone like `LifecyclePhase.Eventually` — an agent host connects lazily and can surface its session types arbitrarily late, which would lock in the wrong fallback. Let the upgrade listener live for the consumer's lifetime instead. -- **Over-commenting**: Don't write long explanatory comments narrating what the code does or justifying ordinary patterns. Per the coding guidelines, only comment code that needs clarification — reserve comments for genuine workarounds/hacks or non-obvious intent, and keep them to a line or two. +- **Over-commenting**: Don't write long explanatory comments narrating what the code does or justifying ordinary patterns. **Hard rules**: JSDoc = 1–2 short sentences max (never enumerate every branch/feature, restate the signature, or list what the function does NOT do); inline method comments = 1 line max, only for a genuine workaround/non-obvious constraint, never to narrate the next statement. Default to no comment — if code needs a paragraph to explain, rename/extract instead. Before writing any comment longer than one line, delete it or shorten it to one line. - **Inserting/removing DOM on demand for transient UI (e.g. inline rename inputs)**: Don't `insertBefore`/`appendChild`+`remove()` a widget on the *tab/row element itself* when an interaction starts/ends — that churns the parent's child list and depends on event ordering during teardown. Also don't eagerly build a heavy widget (e.g. an `InputBox`) per row "just in case", since most rows never use it. Instead, create a **stable, empty container** alongside the label once, toggle its visibility via a CSS class on the row (e.g. `.editing`), and create the widget **inside that container lazily** only while editing — disposing it and emptying the container (`reset(container)`) when done (`InputBox.dispose()` does not detach its own node). Prefer the shared themed widget (`InputBox` + `defaultInputBoxStyles`) over a hand-rolled ``. - **Collapsing distinct provider identities in pickers**: Do not collapse extension-backed chat session ids (e.g. `copilotcli`) and agent-host ids (e.g. `agent-host-copilotcli`) based only on friendly names or well-known provider enums. They can coexist in the Agents window and route to different infrastructure; keep the exact session type id through selection/delegation and hide ambiguous legacy targets when an agent-host target supersedes them. - **Resolving a session's provider via the create-only tracking map**: On the agent host, resolve the owning provider for any per-session operation (createChat, disposeChat, sendMessage, …) through `AgentService._findProviderForSession`, never the raw `_sessionToProvider` map. That map is populated only by `createSession`, so a **restored** session (alive in the state manager after a host restart but never created in this process) is absent from it — a direct lookup throws `no provider for session` and silently breaks the feature (e.g. Add Chat did nothing for restored sessions while messaging worked, because messaging already used the fallback). `_findProviderForSession` falls back to the session URI's scheme provider, which is what makes restored sessions work. - **Dispatching per-chat side-channel actions (agent/model) to the session URI**: An agent-host session can own multiple peer chats, each with its own backend conversation (`CopilotAgent._chatSessions`). Conversation side-channel actions like `SessionAgentChanged`/`SessionModelChanged` must be dispatched to the per-chat **turn channel** (`_resolveTurnDispatchChannel`, which carries a `chatId` fragment for peer chats), not `session.toString()`. The session URI resolves to the session's *default* chat (`_sessions`), so dispatching there silently applies the change to the wrong conversation and an additional chat never sees the agent/model swap. The host must also forward the `chatChannel` through `agentSideEffects.handleAction` → `changeAgent`/`changeModel`, which apply it to `_chatSessions` when present. The protocol models `summary.agent`/`summary.model` at session level only, so equality guards comparing against session summary are valid for the default chat but must be skipped for peer chats. +- **Do not infer or fall back from a peer chat channel after progress was emitted**: Agent progress signals for chat-scoped actions, especially tool-call readiness and permission requests, must be emitted with the exact `ahp-chat://...` channel that owns the tool. Do not recover by scanning active turns, remapping `ChatToolCallConfirmed`, or using `parseDefaultChatUri(...) ?? sessionUri` in `AgentSideEffects`; malformed/misrouted chat channels should fail loudly so the producer or dispatch path is fixed. `handleToolCallConfirmed` and `_toolCallAgents` must use the chat channel URI containing the tool call; keying by the parent session URI makes confirmations miss the pending SDK request. +- **Do not synthesize default chat URIs in the workbench handler**: `AgentHostSessionHandler` must source the upstream default chat URI from hydrated `SessionState.defaultChat` / `SessionState.chats` and store that mapping in its chat-resource-to-upstream-URI map. Calling `buildDefaultChatUri(session)` in the handler assumes one server URI shape and hides protocol/provider bugs; dispatch turn lifecycle and pending/input actions through the mapped upstream chat URI instead. +- **Model subagents as chats, not sessions**: A subagent spawned from a tool call belongs to the parent session as an additional chat with `origin.kind === "tool"`, hidden from the chat tab strip. Do not call `restoreSession` for subagents; that creates `_sessionStates` without a matching `_chatStates` entry, so later chat actions hit "Action for unknown chat". Add a chat on the parent session and dispatch the subagent turn to that chat URI. +- **Keep case-sensitive ids out of URI authority**: URI authorities are case-insensitive, so do not place tool call ids in the `ahp-chat` authority. Subagent chat URIs use a stable `subagent` authority and put the encoded tool call id in the path; use `buildSubagentChatUri(...)` instead of `buildChatUri(..., \`subagent-${toolCallId}\`)`. - **Selected custom agent must be in the SDK's `customAgents`, not just `pluginDirectories`**: The Copilot SDK validates the session-start `agent:` option (passed to `createSession`/`resumeSession`) against the `customAgents` list **by name only** — it does NOT consult `pluginDirectories`. `copilotSessionLauncher._buildSessionConfig` deliberately omits agents from file-dir plugins from `customAgents` (relying on the SDK's `pluginDirectories` discovery to avoid duplicates), so selecting a plugin/extension-contributed agent (e.g. "Inbox") otherwise fails with `Custom agent '' not found`. The fix (`toSdkSessionCustomAgents`) force-adds the resolved selected agent into `customAgents` while every other file-dir agent still loads via `pluginDirectories`. Note the agent picker offers VS Code chat modes from `IChatModeService`, but only `plugin`/`extension` storage agents are synced to the host (`SYNCABLE_STORAGE_SOURCES`); `user`/`local` agents are never synced, so `_resolveAgentName` returns `undefined` for them and no `agent:` is sent. - **Derive SDK custom-agent names exactly like `parseAgentFile`**: `_resolveAgentName` resolves the selected agent through the plugin parser, which trims the frontmatter `name` (`getStringValue('name')?.trim() || nameFromFile`). When building the SDK `customAgents` list (`toSdkCustomAgents`), derive the name the same way (`?.trim() || agent.name`); reading the raw frontmatter `name` without trimming yields a config name that won't match the trimmed `resolvedAgentName`, so the SDK still rejects the session with `Custom agent '' not found`. - **Peer chats have no server `summary`, so dedup side-channel dispatch against the last value sent for that chat**: equality guards before dispatching `SessionModelChanged`/`SessionAgentChanged` compare against `summary.model`/`summary.agent`, which only exist for the session's default chat. For peer chats, track the last-dispatched model/agent on the `AgentHostChatSession` instance (auto-cleaned on dispose) and diff against that — otherwise every peer-chat turn redundantly re-dispatches (and re-resolves the agent), and an intentional "clear selection" (`undefined`) can't be detected. - **Scrollable transcript surfaces must use workbench scrollbars**: Don't make Agents/voice transcript regions scrollable with native `overflow-y: auto` on the content node. Wrap transcript content in `DomScrollableElement`/list widgets so scrollbars match VS Code theming and remain usable in narrow auxiliary-window layouts. +- **Background-sending a multi-chat composer must reset the composer *before* dispatching the send, not concurrently**: in `NewChatInSessionWidget._send`, creating the replacement untitled chat (`openNewChatInSession({ forceNew: true })` → `provider.createNewChat`) and the fire-and-forget background `sendRequest` both reach into shared chat-session state (`acquireOrLoadSession` / `getOrCreateChatSession`) for chats in the **same group**. Running them concurrently (send first, reset second) raced and left the sent chat stuck spinning with its message never dispatched, plus a second empty "New Chat" tab. Fully `await` the composer reset first, then fire the background send so it runs on its own. +- **Chat tab order is the provider's stable creation order; don't reorder in the renderer**: the agent host delivers `state.chats` in stable creation order (append on add, replace-in-place on update — see `agentHostStateManager`/the session reducer), and a genuinely new chat is appended last. The renderer's rebuild autorun (`chatCompositeBar.ts`) must render that order as-is. Do **not** partition/move in-composer `Untitled` chats to the end: a draft is already last, and reordering by status makes a tab jump when a draft commits out of creation order (e.g. sending the 3rd of three drafts first moved it to the front). A chat's `Untitled` presentation (via `AdditionalChat._isNew`, needed so `sessionView.ts` shows the composer) is independent of tab order and must not drive it. Also note `_restorePeerChats` (`agentService.ts`) must seed restored chats in `getChats()` order, not in `Promise.all` resolution order, or the catalog scrambles on reload. +- **A new chat must report `SessionStatus.Untitled` until its first request is sent, regardless of how the provider creates it**: `sessionView.ts` only shows the new-chat composer (which owns the Alt+Enter background-send handler) when `activeChat.status === Untitled`. The agent host commits a new peer chat *eagerly*, so its host status is `Completed` — surfacing the standard chat widget and breaking background send. Gate the chat's presented status on a provider-side `isNew` flag (`AdditionalChat.markNew`/`markSent`, set in `createNewChat` and cleared in `sendRequest`'s committed-chat branch), not on the host-reported status. +- **Service operations should return a result or throw, not `undefined` for unsupported cases**: capability-gated operations like `forkChatInSession` must throw when the provider/session cannot perform them. Keep fallback decisions in the caller before invoking the service instead of encoding fallback as an `undefined` service result. +- **Drop a fork when its turn point is unknown, don't forward it empty**: in `AgentService.createChat`/`createSession`, if the requested fork `turnId`/`turnIndex` resolves to no source turns, set `fork: undefined` and fall through to a fresh create. Forwarding the fork with an empty turn slice makes the Copilot provider call `sessions.fork` with no `toEventId`, inheriting the entire backend conversation while the new chat UI is seeded with zero turns — an inconsistent hidden-history chat. +- **A responsive-layout autorun must re-baseline (not react) to controller-driven restores, holding the flag across the async reveal**: the desktop [D7] responsive sidebar hides the sessions sidebar when small + editor + aux-bar are all open. Switching sessions restores layout via two async paths — the desktop aux-bar restore (`openView`/`openViewContainer`) **and** the base controller's editor working-set apply (`_applyWorkingSet`, which reveals the editor part *after* an `await` and runs on a `Sequencer` microtask). Both reveal parts in a *later* autorun run, so an inline "same-run session changed" check only absorbs the synchronous transition and the async reveal still auto-hid the sidebar on navigation. Fix: a shared base-controller `_withSessionLayoutRestore(work)` epoch wraps **both** restore paths (the working-set wrap is the critical one for non-modal editors); the D7 autorun re-baselines `_previousSpaceConstrained` while `_isRestoringSessionLayout` is true. Also gate the constrained derivation on `!multipleSessionsVisibleObs` so the feature is disabled with multiple sessions visible. Never use a `setTimeout` to bridge the async reveal — tie the flag to the actual promise. +- **A promise-tied "epoch" helper must decrement synchronously for void/sync work, only deferring for real Promises**: `_withSessionLayoutRestore` increments a depth counter, runs `work()`, and decrements when done. If it *always* schedules the decrement on a microtask (`Promise.resolve(result).finally(...)`) — even when `work()` returns `undefined` (the common no-op restore, e.g. a session with no workspace) — the depth stays elevated for the entire synchronous caller/test body, so `_isRestoringSessionLayout` reads `true` forever and the consumer (D7) silently stops acting. Only defer the decrement when `work()` returns a thenable; for void/sync (or throwing) work, decrement in the `finally`. +- **A quick-chat's workspace-less kind is fixed at adapter construction — every construction path must carry `_meta`**: `AgentHostSessionAdapter` resolves its session-kind (`QuickChatSessionKind` vs `WorkspaceSessionKind`) **once**, from `readSessionWorkspaceless(metadata._meta)` in the constructor; `_computeWorkspace()` and `isQuickChat` delegate to that fixed kind and **cannot be flipped by a later `update`/`setMeta`**. So the `_meta.workspaceless` tag must be present in the metadata passed to **every** adapter-construction path: `_refreshSessions()`/`listSessions` **and** the live `_handleSessionAdded(summary)` notification (carry `summary._meta`). Dropping `_meta` on either locks a committed quick chat into `WorkspaceSessionKind` and leaks its `/.copilot/chats/` scratch dir as a `workspace` (breaking the archive-on-delete fallback, list badges, changes/files). On the host, `CopilotAgent.listSessions()` must re-emit `_meta.workspaceless` from persisted `copilot.workspaceless` metadata (mirroring `getSessionMetadata`) so restored sessions carry the tag once the state-manager live summary is gone. The host still keeps the tag on both the summary `_meta` and `SessionState._meta` (`createSessionState(summary)` copies it) so the channels stay consistent. +- **Don't infer "quick chat" from `workspace === undefined`**: a session's workspace observable is `undefined` for genuine quick chats but also transiently/edge-case for workspace-bound sessions, so keying quick-chat UI (context keys, list grouping) on it is imprecise. Expose the intent explicitly via the optional `ISession.isQuickChat: IObservable` (only quick-chat-capable providers set it; absent ⇒ `false`). The agent-host adapter derives it from `readSessionWorkspaceless(_metaObs)`; non-quick-chat providers omit it. Consume it through `isQuickChatSession(session)` / `session.isQuickChat?.read(reader) ?? false`. +- **Workspace-less is inferred from absent `workingDirectory` — exclude forks from that inference**: in `CopilotAgent.createSession`, `isWorkspaceless` must be `!sessionConfig.fork && !sessionConfig.workingDirectory`. A fork that arrives without an explicit `workingDirectory` should inherit the source session's context, not be tagged `copilot.workspaceless` and dropped into a scratch dir + quick-chat system prompt. +- **On a failed quick-chat create, don't activate an unrelated draft**: `SessionsService.openQuickChat` must, on `createQuickChat` throwing, log and return `undefined` without falling back to `_activate(newSession.get())` — that observable can hold a workspace-bound draft from a different call site, so activating it is surprising. Return the activated `IActiveSession` from `_activate`/`openQuickChat` (setActive is synchronous and yields the wrapper) and have the caller focus that exact value, rather than re-reading `activeSession.get()` afterwards. +- **Hide an aux-bar view CONTAINER via `hideIfEmpty: true` + a view `when`, not a container `when`**: `IViewContainerDescriptor` has **no `when`** property (only `IViewDescriptor` does). To conditionally hide a whole container (its tab/title) — e.g. the Agents-window Changes/Files containers for workspace-less sessions — put the context-key `when` on the inner view(s) and set `hideIfEmpty: true` on the container. Container visibility is `hideIfEmpty && activeViewDescriptors.length === 0` (`viewsService.updateViewContainerEnablementContextKey`), and `activeViewDescriptors` already respects each view's `when`, so the container hides reactively when all its views' `when` go false. +- **Don't gate "is this a quick chat?" routing on `isCreated && isQuickChat`**: a quick-chat **draft** is `Untitled`, so `isCreated` (`= status !== Untitled`, `visibleSessions.ts`) is `false` — yet it is still a quick chat. Gating quick-chat routing on `isCreated && isQuickChat` (e.g. the "New" action) makes a draft fall through to the workspace path (scratch-dir composer, no session-type picker, "No models available"). Route on **`isQuickChat` alone** so drafts and committed quick chats behave identically; use `isCreated` only when you genuinely need to distinguish a committed session from an in-composer draft. +- **Cmd+N in the Agents window is a new-**session** gesture only — don't fold quick-chat/peer-chat creation into it**: session creation (**Cmd+N**, `NewChatInSessionsWindowAction`/`workbench.action.sessions.newChat` → always `openNewSession`), quick-chat creation (Chats-section **"+"**, **Cmd+K Cmd+N**, `NewQuickChatAction` → `openQuickChat`), and peer-chat creation (chat **"+"**, **Cmd+T**, `AddChatToSessionAction`) are three **distinct keybindable actions**. Keep them separate — Cmd+N must not become context-aware/mirror-route to a quick chat based on the active session's kind. +- **The New action must never inherit a quick chat's folder into the workspace composer**: `openNewSessionFromActive` seeds the new-session composer with `activeSession.workspace.get()?.uri`. A quick chat is workspace-less by intent, but if its `workspace` observable is ever non-undefined (e.g. a stale-build/coupling leak where `_kind` resolved to `WorkspaceSessionKind` because `_meta.workspaceless` was dropped, exposing the host's scratch `~/.copilot/chats/` cwd as a workspace), Cmd+N would carry that scratch dir into `createNewSession` → "New session in ``", single/no session type, no picker, "No models available". Gate the folder inheritance on `activeSession.isQuickChat` so a quick chat **always** falls to the clean folder-picker composer regardless of any leaked workspace value. +- **A reused new-session composer must re-seed its workspace draft when it swaps out of quick-chat mode**: the session-type picker hides itself when it has no folder types (`sessionTypePicker` `_folderSessionTypes.length === 0`), which is the case whenever the composer has **no active session** (`refresh(undefined)` clears the types). A *freshly opened* new-session composer avoids this by seeding a workspace draft from the restored folder in its constructor — but the same `NewChatWidget` instance is **reused** across the quick-chat→new-session transition (`sessionView.ts` keeps `kind==='newSession'`), and Cmd+N's `openNewSession` discard branch only `_activate(undefined)`, leaving the reused composer session-less → picker hidden. Fix by re-running the constructor's seed (`_seedWorkspaceDraft()`) from an autorun when `_isQuickChatComposer` flips **true→false with no active session**, so the reused composer matches a fresh one (folder + visible picker). Don't assume the constructor-time restore covers a reused composer. +- **Every untitled-session-title fallback must be quick-chat aware**: an untitled session's title observable is `''`, so a hardcoded `localize(…, "New Session")` fallback shows "New Session" even for a quick chat (whose composer says "New Chat"). Route **all** such fallbacks through the shared `getUntitledSessionTitle(isQuickChat)` helper (`services/sessions/common/session.ts`, boolean param so each caller controls reader-tracked `.read(reader)` vs `.get()`). There are ≥5 sites — titlebar (`sessionsTitleBarWidget`), session header (×2: title + rename placeholder), list-row hover (`sessionHoverContent`), sessions picker (`sessionsActions`) — keep them on the helper; never hardcode "New Session". (The Cmd+N *action* title stays "New Session" — that action creates a session, unrelated to a session's own title.) ## Capturing Feedback (meta-rule) Whenever the user flags a wrong pattern, rejects an approach, or gives design/rules feedback, **automatically add it** as a concise pitfall/learning to this `Common Pitfalls` section (or the most relevant spec doc) in the same change — without being asked again. Keep each entry 1–3 sentences: the anti-pattern, why it is wrong, and the preferred pattern. +- **Default/main chat title persistence differs per provider — don't unify them**: For the **agent host**, the default (main) chat title is independent of the session title (`AgentHostSessionAdapter._defaultChatTitleOverride`, persisted on the host as `customChatTitle:`); it must be seeded back on restore via `restoreSession`/`_ensureDefaultChat` (mirroring `_restorePeerChats`), or it reverts to the session title after a process restart / idle eviction. For the **local chat sessions provider** (`localChatSessionsProvider`), the primary chat and the session intentionally **share one `_title` observable**, so renaming the first/main chat updates the session title live — this is by design; do not "fix" it to be independent. Only additional (non-primary) local chats have their own title. + +- **`ISession.capabilities` must be observable, not a live plain getter**: capabilities can hydrate/change after a session first surfaces (e.g. an agent host whose root state arrives after the session's first `SessionState`). A plain getter cannot be tracked by the context-key autorun (`setActiveSessionContextKeys` reads it inside an `autorun`), so `supportsMultipleChats`/`sessionSupportsFork` would stay stale, and a multi-chat catalog processed while `supportsMultipleChats` was still `false` would stay collapsed to `[defaultChat]`. Expose `capabilities` as `IObservable` (agent host derives it from `connection.rootState` via `observableFromEvent` + `derivedOpts` with `structuralEquals`; static providers use `constObservable`), have consumers read `.read(reader)`/`.get()`, and re-apply the chat catalog from the last `SessionState` in an `autorun` on capability change. Do **not** fix this by firing `_onDidChangeSessions` — the active-session context autorun tracks the session's own observables, not the provider's session-list event. + ## Validating Changes You **must** run these checks before declaring work complete: diff --git a/.github/workflows/chat-perf.yml b/.github/workflows/chat-perf.yml index 8f67c576e02a1..8713e9a567ec0 100644 --- a/.github/workflows/chat-perf.yml +++ b/.github/workflows/chat-perf.yml @@ -57,13 +57,19 @@ env: SCENARIOS_INPUT: ${{ inputs.scenarios || '' }} TEST_SETTINGS_INPUT: ${{ inputs.test_settings || '' }} BASELINE_SETTINGS_INPUT: ${{ inputs.baseline_settings || '' }} + # Skip binary downloads during `npm ci` (they hang intermittently on hosted + # runners inside the nested postinstall installs). Electron and Playwright are + # fetched explicitly later via `Download Electron` and `Install Playwright + # Chromium`, which ignore these flags. Mirrors pr-node-modules.yml. + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 jobs: # ── Shared setup: build once, cache everything ────────────────────── setup: name: Build & Cache runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 50 outputs: test_is_version: ${{ steps.resolve.outputs.is_version }} test_build_arg: ${{ steps.resolve.outputs.build_arg }} @@ -111,13 +117,33 @@ jobs: libasound2t64 libxshmfence1 libgtk-3-0 - name: Install dependencies - run: npm ci + run: | + set -e + # Retry with a per-attempt timeout to survive transient npm registry + # hangs/failures on hosted runners (mirrors pr-node-modules.yml). + for i in {1..5}; do + timeout 900 npm ci && break + if [ $i -eq 5 ]; then + echo "npm ci failed/hung too many times" >&2 + exit 1 + fi + echo "npm ci attempt $i failed or timed out, retrying..." + done env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install build dependencies - run: npm ci working-directory: build + run: | + set -e + for i in {1..5}; do + timeout 900 npm ci && break + if [ $i -eq 5 ]; then + echo "npm ci failed/hung too many times" >&2 + exit 1 + fi + echo "npm ci attempt $i failed or timed out, retrying..." + done - name: Transpile source run: npm run transpile-client @@ -210,7 +236,16 @@ jobs: libasound2t64 libxshmfence1 libgtk-3-0 - name: Install dependencies - run: npm ci + run: | + set -e + for i in {1..5}; do + timeout 900 npm ci && break + if [ $i -eq 5 ]; then + echo "npm ci failed/hung too many times" >&2 + exit 1 + fi + echo "npm ci attempt $i failed or timed out, retrying..." + done env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -383,7 +418,16 @@ jobs: libasound2t64 libxshmfence1 libgtk-3-0 - name: Install dependencies - run: npm ci + run: | + set -e + for i in {1..5}; do + timeout 900 npm ci && break + if [ $i -eq 5 ]; then + echo "npm ci failed/hung too many times" >&2 + exit 1 + fi + echo "npm ci attempt $i failed or timed out, retrying..." + done env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index 971fda8191f1e..281f019ad283d 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -122,6 +122,8 @@ jobs: if: ${{ inputs.electron_tests && inputs.unit_and_integration_tests }} timeout-minutes: 15 run: ./scripts/test.sh --tfs "Unit Tests" + env: + VSCODE_SKIP_PRELAUNCH: '1' - name: 🧪 Run unit tests (node.js) if: ${{ inputs.electron_tests && inputs.unit_and_integration_tests }} @@ -161,6 +163,8 @@ jobs: if: ${{ inputs.electron_tests && inputs.unit_and_integration_tests }} timeout-minutes: 20 run: ./scripts/test-integration.sh --tfs "Integration Tests" + env: + VSCODE_SKIP_PRELAUNCH: '1' - name: 🧪 Run integration tests (Browser, Webkit) if: ${{ inputs.browser_tests && inputs.unit_and_integration_tests }} @@ -171,6 +175,8 @@ jobs: if: ${{ inputs.remote_tests && inputs.unit_and_integration_tests }} timeout-minutes: 20 run: ./scripts/test-remote-integration.sh + env: + VSCODE_SKIP_PRELAUNCH: '1' - name: Compile smoke tests if: ${{ inputs.smoke_tests }} diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index 113d41cdda3bb..65dd148287a68 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -209,6 +209,48 @@ jobs: FONTCONFIG_EOF echo "FONTCONFIG_FILE=/tmp/fonts-minimal.conf" >> "$GITHUB_ENV" + # Root-cause fix for the intermittent Electron startup SIGSEGV on Linux CI. + # Symbolizing the crash dumps shows the fault is a Pango concurrency bug, not + # a VS Code bug: + # + # pango: fc_thread_func -> init_in_thread -> FcInit() (pangofc-fontmap.c) + # fontconfig: FcInit -> FcConfigParseAndLoadFromMemory -> _FcConfigParse + # libexpat: XML_ParseBuffer -> libc (NULL deref, SIGSEGV) + # + # Pango >= 1.52's pango_fc_font_map_init() unconditionally spawns a + # "[pango] fontconfig" thread that runs FcInit(). That races against the + # Electron/Chromium main thread's own fontconfig use during startup and + # corrupts fontconfig's global config while it is being parsed. There is no + # env var to disable the Pango thread, and it is still present in Pango 1.56. + # + # Eliminate the race by initializing fontconfig once, single-threaded, from + # an ELF constructor that runs before main() (and therefore before any thread + # exists). Pango's later threaded FcInit() then finds fontconfig already + # initialized and returns immediately, so the concurrent parse never happens. + - name: Pre-initialize fontconfig to avoid Pango threaded FcInit crash + run: | + set -euo pipefail + cat > "$RUNNER_TEMP/fcpreinit.c" <<'CEOF' + #define _GNU_SOURCE + #include + + __attribute__((constructor)) + static void preinit_fontconfig(void) + { + void *handle = dlopen("libfontconfig.so.1", RTLD_NOW | RTLD_GLOBAL); + if (handle) { + int (*fc_init)(void) = (int (*)(void)) dlsym(handle, "FcInit"); + if (fc_init) { + fc_init(); + } + } + } + CEOF + gcc -shared -fPIC -O2 -o "$RUNNER_TEMP/libfcpreinit.so" "$RUNNER_TEMP/fcpreinit.c" -ldl + echo "Built fontconfig pre-init shim at $RUNNER_TEMP/libfcpreinit.so" + # Preload it for every subsequent step that launches Electron/Chromium. + echo "LD_PRELOAD=$RUNNER_TEMP/libfcpreinit.so${LD_PRELOAD:+:$LD_PRELOAD}" >> "$GITHUB_ENV" + - name: Fontconfig diagnostics and cache reset run: | set -e @@ -244,6 +286,12 @@ jobs: echo "" echo "--- fontconfig version ---" fc-cache --version 2>&1 | head -1 + echo "" + echo "--- fontconfig pre-init shim (LD_PRELOAD) ---" + echo "LD_PRELOAD=${LD_PRELOAD:-}" + if [ -n "${LD_PRELOAD:-}" ]; then + ls -l "${LD_PRELOAD%%:*}" 2>/dev/null || true + fi continue-on-error: true - name: Transpile client and extensions @@ -276,6 +324,7 @@ jobs: run: ./scripts/test.sh --tfs "Unit Tests" env: DISPLAY: ":10" + VSCODE_SKIP_PRELAUNCH: '1' - name: 🧪 Run unit tests (node.js) if: ${{ inputs.electron_tests && inputs.unit_and_integration_tests }} @@ -317,6 +366,7 @@ jobs: run: ./scripts/test-integration.sh --tfs "Integration Tests" env: DISPLAY: ":10" + VSCODE_SKIP_PRELAUNCH: '1' - name: 🧪 Run integration tests (Browser, Chromium) if: ${{ inputs.browser_tests && inputs.unit_and_integration_tests }} @@ -329,6 +379,7 @@ jobs: run: ./scripts/test-remote-integration.sh env: DISPLAY: ":10" + VSCODE_SKIP_PRELAUNCH: '1' - name: Compile smoke tests if: ${{ inputs.smoke_tests }} diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index 45681aff7077b..f1524e55fcc7d 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -130,6 +130,8 @@ jobs: timeout-minutes: 15 shell: pwsh run: .\scripts\test.bat --tfs "Unit Tests" + env: + VSCODE_SKIP_PRELAUNCH: '1' - name: 🧪 Run unit tests (node.js) if: ${{ inputs.electron_tests && inputs.unit_and_integration_tests }} @@ -181,6 +183,8 @@ jobs: timeout-minutes: 20 shell: pwsh run: .\scripts\test-integration.bat --tfs "Integration Tests" + env: + VSCODE_SKIP_PRELAUNCH: '1' - name: 🧪 Run integration tests (Browser, Chromium) if: ${{ inputs.browser_tests && inputs.unit_and_integration_tests }} @@ -193,6 +197,8 @@ jobs: timeout-minutes: 20 shell: pwsh run: .\scripts\test-remote-integration.bat + env: + VSCODE_SKIP_PRELAUNCH: '1' - name: Diagnostics after integration test runs if: ${{ inputs.unit_and_integration_tests && always() }} diff --git a/.github/workflows/require-commit-trailer.yml b/.github/workflows/require-commit-trailer.yml new file mode 100644 index 0000000000000..5b4ccc8606b39 --- /dev/null +++ b/.github/workflows/require-commit-trailer.yml @@ -0,0 +1,53 @@ +name: Require commit trailer + +on: + pull_request: + branches: + - 'release/msrc/*' + +permissions: {} + +jobs: + check-trailer: + name: Check for trailer + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Verify PR has a single commit + env: + COMMIT_COUNT: ${{ github.event.pull_request.commits }} + run: | + if [ "$COMMIT_COUNT" != "1" ]; then + echo "::error::This PR has $COMMIT_COUNT commits. release/msrc/* PRs must contain exactly one commit. Please squash your commits." + exit 1 + fi + echo "PR has a single commit." + - name: Verify trailer on HEAD commit + shell: python + run: | + import re + import subprocess + import sys + + sha = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip() + trailers = subprocess.check_output( + ['git', 'log', '-1', '--pretty=format:%(trailers:key=Msrc-Case-Id,valueonly=true)', 'HEAD'], + text=True, + ) + values = [line.strip() for line in trailers.splitlines() if line.strip()] + + if not values: + print(f"::error::Commit {sha} is missing the required 'Msrc-Case-Id' trailer.") + print("Add a trailer to the commit message, for example:\n\n Msrc-Case-Id: 12345\n") + sys.exit(1) + + if not all(re.fullmatch(r'\d+', value) for value in values): + print(f"::error::Commit {sha} has an invalid 'Msrc-Case-Id' trailer value. Expected a number.") + print("Use a numeric case id, for example:\n\n Msrc-Case-Id: 12345\n") + sys.exit(1) + + print(f"Commit {sha} has the required 'Msrc-Case-Id' trailer.") diff --git a/.github/workflows/sync-cnb.yml b/.github/workflows/sync-cnb.yml new file mode 100644 index 0000000000000..208441719b6c1 --- /dev/null +++ b/.github/workflows/sync-cnb.yml @@ -0,0 +1,25 @@ +# .github/workflows/sync-cnb.yml + +name: Sync to CNB +on: [push] + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Sync to CNB Repository + uses: docker://tencentcom/git-sync + env: + # 注入 Git LFS 配置,消除锁定提示 + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: "lfs.https://cnb.cool/soucod-github/soucod/vscode.git/info/lfs.locksverify" + GIT_CONFIG_VALUE_0: "true" + PLUGIN_TARGET_URL: "https://cnb.cool/soucod-github/soucod/vscode.git" + PLUGIN_AUTH_TYPE: "https" + PLUGIN_USERNAME: "cnb" + PLUGIN_PASSWORD: ${{ secrets.GIT_CNB_TOKEN }} + PLUGIN_FORCE: "true" diff --git a/.ide/Dockerfile b/.ide/Dockerfile new file mode 100644 index 0000000000000..6dae62645f611 --- /dev/null +++ b/.ide/Dockerfile @@ -0,0 +1,61 @@ +# .ide/Dockerfile + +# 可将 node 替换为需要的基础镜像 +FROM docker.cnb.cool/avwq/cnb.cool/cnb-avwq-default-dev-env-centos-stream:stream10 + +# 安装 code-server 和 vscode 常用插件 +RUN curl -fsSL https://code-server.dev/install.sh | sh \ + && code-server --install-extension cnbcool.cnb-welcome \ + && code-server --install-extension redhat.vscode-yaml \ + && code-server --install-extension dbaeumer.vscode-eslint \ + && code-server --install-extension waderyan.gitblame \ + && code-server --install-extension mhutchie.git-graph \ + && code-server --install-extension donjayamanne.githistory \ + && code-server --install-extension tencent-cloud.coding-copilot \ + && echo done + +# 安装 ssh 服务,用于支持 VSCode 等客户端通过 Remote-SSH 访问开发环境(也可按需安装其他软件) +RUN dnf install -y dnf-plugins-core && dnf config-manager --set-enabled crb +RUN dnf update -y && dnf install -y git wget unzip openssh-server epel-release curl vim unzip procps && dnf install -y atop btop htop iftop iotop cockpit +# 业务安装 +RUN dnf install -y nodejs24 npm python && dnf groupinstall "Development Tools" -y && dnf install -y libX11-devel libxkbfile libsecret-devel krb5-devel && dnf install -y fakeroot rpm +RUN dnf install -y libxkbfile-devel libX11-devel pkgconf-pkg-config gcc-c++ make python3 + +alternatives --install /usr/bin/node node /usr/bin/node-24 240 +alternatives --install /usr/bin/npm npm /usr/bin/npm-24 240 + +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone +RUN npm i -g pnpm + +RUN dnf install -y \ + atk \ + gtk3 \ + gtk3-devel \ + libX11 \ + libXScrnSaver \ + libnotify \ + nss \ + alsa-lib \ + cups-libs \ + libXcomposite \ + libXdamage \ + libXext \ + libXfixes \ + libXrandr \ + libgbm \ + libdrm \ + mesa-libgbm + + +# 创建 `/ide_cnb` 目录,用于安装 IDE。系统通过此目录自动识别已安装的 IDE,安装路径必须为 `/ide_cnb`。 +RUN mkdir -p /ide_cnb +# WebStorm +RUN wget https://download.jetbrains.com/webstorm/WebStorm-2026.1.tar.gz \ + && tar -zxvf WebStorm-2026.1.tar.gz -C /ide_cnb \ + && rm WebStorm-2026.1.tar.gz + + + +# 指定字符集支持命令行输入中文(根据需要选择字符集) +ENV LANG C.UTF-8 +ENV LANGUAGE C.UTF-8 diff --git a/.mailmap b/.mailmap index 5bd99619330ac..9c4139cbdd3d2 100644 --- a/.mailmap +++ b/.mailmap @@ -1,3 +1,2 @@ Raymond Zhao Tyler Leonhardt Tyler Leonhardt -João Moreno João Moreno diff --git a/.npmrc b/.npmrc index 8c21e58ef14d5..b6b2ee025a57d 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="42.3.0" -ms_build_id="14159160" +target="42.5.0" +ms_build_id="14525058" runtime="electron" ignore-scripts=false build_from_source="true" diff --git a/.nvmrc b/.nvmrc index 5bf4400f22922..1dd37d53743d1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.15.0 +24.17.0 diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 0b26098f8edaf..b2c13bf7d1380 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.126.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" + "value": "$MILESTONE=milestone:\"1.127.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index d7a3f66d69a06..201378fdc8eb7 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.126.0\"\n\n$MINE=assignee:@me" + "value": "$MILESTONE=milestone:\"1.127.0\"\n\n$MINE=assignee:@me" }, { "kind": 2, diff --git a/.vscode/settings.json b/.vscode/settings.json index c0daea168a8f5..7d69353d3a690 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -157,6 +157,7 @@ ], "githubPullRequests.codingAgent.enabled": true, "githubPullRequests.codingAgent.uiIntegration": true, + "githubPullRequests.enableAttestationCommits": true, // --- Testing & Debugging --- "testing.autoRun.mode": "rerun", "debug.javascript.terminalOptions": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e80188ca5ca23..807b327404732 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,111 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Install & Watch (Client)", + "detail": "Install dependencies, compile and watch VS Code core for fixing a bug or implementing a feature.", + "dependsOn": [ + "Install Dependencies", + "Compile", + "Watch Client Transpile" + ], + "dependsOrder": "sequence", + "inAgents": true, + "problemMatcher": [] + }, + { + "label": "Run VS Code", + "detail": "Run VS Code with a user data directory inside the worktree.", + "type": "shell", + "command": "./scripts/code.sh", + "windows": { + "command": ".\\scripts\\code.bat" + }, + "args": [ + "--user-data-dir=${workspaceFolder}/.profile-oss", + "--extensions-dir=${workspaceFolder}/.profile-oss/extensions" + ], + "inAgents": true, + "problemMatcher": [] + }, + { + "label": "Install & Watch (All)", + "detail": "Install dependencies and watch all of VS Code (core, extensions and copilot).", + "dependsOn": [ + "Install Dependencies", + "Watch" + ], + "dependsOrder": "sequence", + "inAgents": true, + "problemMatcher": [] + }, + { + "label": "Install Dependencies", + "type": "shell", + "command": "npm install", + "windows": { + "command": "cmd /c \"npm install\"" + }, + "presentation": { + "reveal": "always", + "group": "session", + "close": false + }, + "problemMatcher": [] + }, + { + "label": "Compile", + "type": "npm", + "script": "compile", + "presentation": { + "reveal": "always", + "group": "session", + "close": false + }, + "problemMatcher": "$tsc" + }, + { + "label": "Watch", + "type": "npm", + "script": "watch", + "isBackground": true, + "presentation": { + "reveal": "always", + "group": "session", + "close": false + }, + "problemMatcher": [] + }, + { + "label": "Watch Client Transpile", + "type": "npm", + "script": "watch-client-transpile", + "isBackground": true, + "presentation": { + "reveal": "always", + "group": "session", + "close": false + }, + "problemMatcher": { + "owner": "esbuild", + "applyTo": "closedDocuments", + "fileLocation": [ + "relative", + "${workspaceFolder}/src" + ], + "pattern": { + "regexp": "^(.+?):(\\d+):(\\d+): ERROR: (.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + }, + "background": { + "beginsPattern": "Starting transpilation\\.\\.\\.", + "endsPattern": "Finished transpilation with" + } + } + }, { "type": "npm", "script": "watch-client-transpiled", @@ -294,7 +399,6 @@ "Run Dev Agents" ], "dependsOrder": "sequence", - "inAgents": true, "problemMatcher": [] }, { @@ -304,7 +408,6 @@ "Run Dev" ], "dependsOrder": "sequence", - "inAgents": true, "problemMatcher": [] }, { @@ -372,6 +475,35 @@ "reveal": "never" } }, + { + "label": "Run VS Code (Web)", + "detail": "Run the Agents window as a web client.", + "type": "shell", + "command": "./scripts/code-server.sh", + "windows": { + "command": ".\\scripts\\code-server.bat" + }, + "args": [ + "--connection-token", + "dev-token", + "--port", + "8080" + ], + "inAgents": true, + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "" + }, + "background": { + "beginsPattern": ".*node .*", + "endsPattern": "Web UI available at .*" + } + }, + "presentation": { + "reveal": "always" + } + }, { "type": "npm", "script": "eslint", @@ -445,8 +577,7 @@ "windows": { "command": ".\\scripts\\code.bat" }, - "problemMatcher": [], - "inAgents": true + "problemMatcher": [] }, { "type": "npm", @@ -477,8 +608,7 @@ "beginsPattern": "Starting compilation\\.\\.\\.", "endsPattern": "Finished compilation with" } - }, - "inAgents": true + } }, { "label": "Component Explorer Server", @@ -509,10 +639,6 @@ "windows": { "command": "cmd /c \"npm install && npm run watch-transpile\"" }, - "inAgents": true, - "runOptions": { - "runOn": "worktreeCreated" - }, "presentation": { "focus": false, "reveal": "never" diff --git a/build/.cachesalt b/build/.cachesalt index df1d8c82aab64..d382d477ac6c7 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2026-06-19T12:30:00.000Z +2026-06-22T15:10:10.546Z diff --git a/build/agent-sdk/agents/claude/package-lock.json b/build/agent-sdk/agents/claude/package-lock.json index c5f767a870a81..d7d9cfd7b29c6 100644 --- a/build/agent-sdk/agents/claude/package-lock.json +++ b/build/agent-sdk/agents/claude/package-lock.json @@ -6,26 +6,26 @@ "": { "name": "agent-sdk-claude", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.3.169" + "@anthropic-ai/claude-agent-sdk": "0.3.198" } }, "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.169.tgz", - "integrity": "sha512-pzsd0BBnlfHwAUfKouSNwDLcCxGDq+lbPZTsIdiNSLw+AQw7mjxPxqnFwd4YH7qw+E3XhlotuihCm01XzcybPQ==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.198.tgz", + "integrity": "sha512-xt469sSCyclTPtzpLAg0Aschy665GiRMgZKabSmESbGUA5/H56HcILVOiFxclXswkeMUk2fQxfHJUY9UZfiTnA==", "license": "SEE LICENSE IN README.md", "engines": { "node": ">=18.0.0" }, "optionalDependencies": { - "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.169", - "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.169", - "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.169", - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.169", - "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.169", - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.169", - "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.169", - "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.169" + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.198", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.198", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.198", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.198", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.198", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.198", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.198", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.198" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", @@ -34,9 +34,9 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.169.tgz", - "integrity": "sha512-5PJU3wqfvJUrgUF3H7iroxAGDyliKmq5q8kK+glERf8YZgmPeDMvQL4mGSB0Vf9RgBWS01dCen93TIsN0O8NPQ==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.198.tgz", + "integrity": "sha512-ZmiAybQKIKcP1qEAE/vfXvfxtKxG9CnJn98QTXC5Zxiwuy7Mllx2ALXh9dfmsf0V87CGEodlZQmMgUJotNIsUw==", "cpu": [ "arm64" ], @@ -47,9 +47,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.169.tgz", - "integrity": "sha512-1Hzt0QYG7N/ExfPbN/C92YRc3BoDnyQMJpuWLVyI/r0NmKV/se/cZbIygbvQXs2ywoXOB/EyNj/hkVHaC1G22g==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.198.tgz", + "integrity": "sha512-XwH5vgN46WSwg8aC1OagNofnJpV/G1ciEu118GEKer8ZhVkq/dvK/DqShxMkb6r1jV7u5IJ7zPXu9uKliyNJAw==", "cpu": [ "x64" ], @@ -60,9 +60,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.169.tgz", - "integrity": "sha512-J0tyM4t5qxc2/xilsfHqcJv+fyEDhX66Nv+CMXq4mRbquZmMHhbZbh8ryb+BnKYXUeqKX3uoU/J+7K6/fjcY4g==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.198.tgz", + "integrity": "sha512-qmz8dxEtDIlKntU5qYe0R4aWTxTue5S7zIQknatLX7aJ6HN/nq1aCNXWn5smTH2FViBkUPPR+sCIsNwSk6AT6Q==", "cpu": [ "arm64" ], @@ -76,9 +76,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.169.tgz", - "integrity": "sha512-xkmyNdU6f7HXXzgR6IZym7x41bjL3rjGrXf5PAmx9PjdThy476+TrpO1evKUza1zlVmUj4tj/jYKQsV4ER7QFw==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.198.tgz", + "integrity": "sha512-Q7lKVNjIrUQ2B/AR77OvRf0zeOdEjonFVaR9FYrrwtzGeEqum69WSht5nM7Y7el3wjbNi0/eV0QTUM0DlsTEfw==", "cpu": [ "arm64" ], @@ -92,9 +92,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.169.tgz", - "integrity": "sha512-ktEPBwzXZagt0cntfRLlvNL0sAplt2HOCbR5iBvq2IV5dLvbEOiBjZQArIcSAN7CTQUFqkZs9HzQMdKe5QPwuQ==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.198.tgz", + "integrity": "sha512-Zqxyz2AT1UM5WlOOoLJhLssZDgZo8rBK5ku6daveK12zp+UTJGZhGsjFghz1/ASxH08KqOTbUePNTORnPhHAEQ==", "cpu": [ "x64" ], @@ -108,9 +108,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.169.tgz", - "integrity": "sha512-Me8VW91pCKgkct/y1isjKNVt+ZPMPhHk3C7O078UO1bakHD6kuPiMtugcCsg4jmmmuuywXQIQUESRbcf0GluUg==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.198.tgz", + "integrity": "sha512-h1SrWVIMjLInYNPlf+TxXuKTOdoiOfJLBSoQG97315Z2Nh0IpBfqWExlqYTtPCgKE7q2iga31U283QfHpIDlSQ==", "cpu": [ "x64" ], @@ -124,9 +124,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.169.tgz", - "integrity": "sha512-HL9ecP82A/H8aC896Q7ETyl+nLFkMzBHfO0yvK7Lhxbi98CEpqXn3BADTAc5pEgzkO+r18+CDmZdgjTFMvGoTw==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.198.tgz", + "integrity": "sha512-mjIHf1HFiRuXefewWTaNZFlTZlCaEt/xsRjc1nSTCEEpFolZayVhrDKz+O2QFVcDtPl8x8GeYSL0kiikg1DZjQ==", "cpu": [ "arm64" ], @@ -137,9 +137,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { - "version": "0.3.169", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.169.tgz", - "integrity": "sha512-34pBbiKe7FNsY8LHuuTmKujmYko/cBOrKJpk9H+MOot+dSfB/FmIHq9USL17otNal5Droq9lqpezAWPBPv3lAQ==", + "version": "0.3.198", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.198.tgz", + "integrity": "sha512-y3HLuCCz1kDwUrhd6OnqO+d5BUpTFSzNUsPT9kf3r1vk9HYKF+eMC9eIlcOhiW2kX491kxEvuEOfqgIkGx15cg==", "cpu": [ "x64" ], diff --git a/build/agent-sdk/agents/claude/package.json b/build/agent-sdk/agents/claude/package.json index 6684cedf37720..bcdae1739cfbe 100644 --- a/build/agent-sdk/agents/claude/package.json +++ b/build/agent-sdk/agents/claude/package.json @@ -3,6 +3,6 @@ "private": true, "comment": "Pinned dependency set for the build/agent-sdk claude tarball. The package-lock.json alongside is the source of truth for transitive deps — produce.ts runs `npm ci` against this directory to get byte-deterministic output across pipeline runs.", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.3.169" + "@anthropic-ai/claude-agent-sdk": "0.3.198" } } diff --git a/build/agent-sdk/agents/codex/package-lock.json b/build/agent-sdk/agents/codex/package-lock.json index 23a40dc300728..f526dd5a5416f 100644 --- a/build/agent-sdk/agents/codex/package-lock.json +++ b/build/agent-sdk/agents/codex/package-lock.json @@ -6,13 +6,13 @@ "": { "name": "agent-sdk-codex", "dependencies": { - "@openai/codex": "0.135.0" + "@openai/codex": "0.142.0" } }, "node_modules/@openai/codex": { - "version": "0.135.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.135.0.tgz", - "integrity": "sha512-ID75QEYmAT1WsUQmpxPlNsL5W1a+2eeD7fP6ywdwGseiXUG8D5i16L+dzbr8MT+2oTkaVqzOdvAqVOCeV/H/Bw==", + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0.tgz", + "integrity": "sha512-c7WftbRyE4zOLJV5p73mcGn4jVK3FmK84Q65hrZrXboZzLPDWRfbFedCbsIjU4PJS/kvgyMPhv7dH4/nhXzUyg==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -21,19 +21,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.135.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.135.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.135.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.135.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.135.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.135.0-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.0-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.0-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.0-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.142.0-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.0-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.142.0-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.135.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.135.0-darwin-arm64.tgz", - "integrity": "sha512-wpNzssusKfrldVlq39+HyQh1wCyc9SQNpHdAFGKtPenrgRte4Ct8/oVsDtKWuFZsqLBFwbL4MrzrevnB63+9HA==", + "version": "0.142.0-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-darwin-arm64.tgz", + "integrity": "sha512-jwbriCRTSNfqoGov5bNnnYAKarnNv4QfGkut8psKAajebL6VPI2ZjCxCJ1OFA5HhUQsN8GQgEjKlj6WhYcMmUA==", "cpu": [ "arm64" ], @@ -48,9 +48,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.135.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.135.0-darwin-x64.tgz", - "integrity": "sha512-ZrjAqce23lbv9KfkYOhElf1lTI+SysXmyGM0FV5u4+PBCKPkkEs4eaS3H8Uig0i4bUSu1QylrOOCskzYhZ6VyQ==", + "version": "0.142.0-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-darwin-x64.tgz", + "integrity": "sha512-qwdPfW8sOBEfWw1Rt7baveUsOjrudrM7qfKNnJvHPRrwtt4jemTd22VtZ6r02kUQdLwt6Ddvs0Pwzk6QJW6PqA==", "cpu": [ "x64" ], @@ -65,9 +65,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.135.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.135.0-linux-arm64.tgz", - "integrity": "sha512-dM+cv5ZL+BgIQzEIvMg9AxZ98n5lkKLgtp5zJLXWSrbCllbnUSqxYMUiWI5c1a1uBDUtkbY9fcGKXFLf+d+gyg==", + "version": "0.142.0-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-linux-arm64.tgz", + "integrity": "sha512-gX9hCK59bE0Itnous1MCrKiXTYuoGVE6oJUE0kyRSzaBD267sh9TNR3DXKbY7TsP7iAs19n8YXDJNdHZJtakSQ==", "cpu": [ "arm64" ], @@ -82,9 +82,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.135.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.135.0-linux-x64.tgz", - "integrity": "sha512-5EosY67yU28UJSnl/obdN2F1CDaimYbzm9SLR8dwwzkeBBnY6dHgAKJ2GTu9Nc8CmgmtVFBGzgPqehsIcueVvA==", + "version": "0.142.0-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-linux-x64.tgz", + "integrity": "sha512-nywS+ogFPRhZnQnKL+jzWKzFhmjTQbYPh6n8dtQ/E+sJ3FoZVcb9a6kvHL5yjgtlCiDkP0jPDYY5AK3tDShYyw==", "cpu": [ "x64" ], @@ -99,9 +99,9 @@ }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.135.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.135.0-win32-arm64.tgz", - "integrity": "sha512-SAeR+CUv7KWwE6eTc2UFaFjo6FpHywYfDFKrK6FqLms1rq1NPju2SoX7rhM6UEew/lUx2mdZv/LDs11s/N/Qgg==", + "version": "0.142.0-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-win32-arm64.tgz", + "integrity": "sha512-Lz8xEn7uNn0XTi8k0jApKM5P9BD7QvAH3ZbtlGi1zfSOgh1kmgpfXxaTlJF503mozdHaA2r6UA7hfTi+bI0CvQ==", "cpu": [ "arm64" ], @@ -116,9 +116,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.135.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.135.0-win32-x64.tgz", - "integrity": "sha512-uYwUBMbOfmVlCESJZmZsOG+cYwNFYvkMbQ+FB6C1u9RYz0m3mZeYNN0j+l1hRSyUgPMFJHzNpgNx1Usal5QZFQ==", + "version": "0.142.0-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-win32-x64.tgz", + "integrity": "sha512-LkARkXh3NM0XA8R8hFAgjx017fadfb9RgmqLzdhO1Qt/bDVSJnmDf4cwJ2h+FWZcm9/rRk3JC/IuqBvwg/TN+Q==", "cpu": [ "x64" ], diff --git a/build/agent-sdk/agents/codex/package.json b/build/agent-sdk/agents/codex/package.json index b7c1b24f3a085..5ae85c37e911b 100644 --- a/build/agent-sdk/agents/codex/package.json +++ b/build/agent-sdk/agents/codex/package.json @@ -3,6 +3,6 @@ "private": true, "comment": "Pinned dependency set for the build/agent-sdk codex tarball. The package-lock.json alongside is the source of truth for transitive deps — produce.ts runs `npm ci` against this directory to get byte-deterministic output across pipeline runs.", "dependencies": { - "@openai/codex": "0.135.0" + "@openai/codex": "0.142.0" } } diff --git a/build/azure-pipelines/alpine/product-build-alpine-cli.yml b/build/azure-pipelines/alpine/product-build-alpine-cli.yml index 4a8bc36e0ecf9..3081390b6a8c2 100644 --- a/build/azure-pipelines/alpine/product-build-alpine-cli.yml +++ b/build/azure-pipelines/alpine/product-build-alpine-cli.yml @@ -34,6 +34,13 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub token" + inputs: + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" + - template: ../cli/cli-apply-patches.yml@self - script: | @@ -41,7 +48,7 @@ jobs: npm ci workingDirectory: build env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install build dependencies - task: Npm@1 diff --git a/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml b/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml index 07f1545ca2483..7b560b328677b 100644 --- a/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml +++ b/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml @@ -25,9 +25,9 @@ jobs: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -88,11 +88,20 @@ jobs: mkdir -p .build/nodejs-musl NODE_VERSION=$(grep '^target=' remote/.npmrc | cut -d '"' -f 2) BUILD_ID=$(grep '^ms_build_id=' remote/.npmrc | cut -d '"' -f 2) - gh release download "v${NODE_VERSION}-${BUILD_ID}" -R microsoft/vscode-node -p "node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" --dir .build/nodejs-musl --clobber + az extension add --name azure-devops --upgrade --only-show-errors + az artifacts universal download \ + --organization "https://dev.azure.com/monacotools" \ + --project "Monaco" \ + --scope project \ + --feed "vscode-node" \ + --name "node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar" \ + --version "${NODE_VERSION}-${BUILD_ID}" \ + --path .build/nodejs-musl \ + --only-show-errors tar -xzf ".build/nodejs-musl/node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" -C ".build/nodejs-musl" --strip-components=1 rm ".build/nodejs-musl/node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + AZURE_DEVOPS_EXT_PAT: "$(System.AccessToken)" displayName: Download NodeJS MUSL condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -111,7 +120,7 @@ jobs: npm_config_arch: $(NPM_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" VSCODE_NPMRC_PATH: $(NPMRC_PATH) diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index 3359224d3ed8d..8151a3397af5a 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -67,9 +67,9 @@ jobs: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -134,11 +134,20 @@ jobs: mkdir -p .build/nodejs-musl NODE_VERSION=$(grep '^target=' remote/.npmrc | cut -d '"' -f 2) BUILD_ID=$(grep '^ms_build_id=' remote/.npmrc | cut -d '"' -f 2) - gh release download "v${NODE_VERSION}-${BUILD_ID}" -R microsoft/vscode-node -p "node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" --dir .build/nodejs-musl --clobber + az extension add --name azure-devops --upgrade --only-show-errors + az artifacts universal download \ + --organization "https://dev.azure.com/monacotools" \ + --project "Monaco" \ + --scope project \ + --feed "vscode-node" \ + --name "node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar" \ + --version "${NODE_VERSION}-${BUILD_ID}" \ + --path .build/nodejs-musl \ + --only-show-errors tar -xzf ".build/nodejs-musl/node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" -C ".build/nodejs-musl" --strip-components=1 rm ".build/nodejs-musl/node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + AZURE_DEVOPS_EXT_PAT: "$(System.AccessToken)" displayName: Download NodeJS MUSL condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -157,7 +166,7 @@ jobs: npm_config_arch: $(NPM_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" VSCODE_NPMRC_PATH: $(NPMRC_PATH) @@ -194,7 +203,7 @@ jobs: - script: npm run gulp core-ci env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Compile - script: npx deemon --attach -- node build/azure-pipelines/common/downloadCopilotVsix.ts @@ -214,7 +223,8 @@ jobs: echo "##vso[task.setvariable variable=SERVER_DIR_PATH]$DIR_PATH" echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build server - script: | @@ -229,5 +239,6 @@ jobs: echo "##vso[task.setvariable variable=WEB_DIR_PATH]$DIR_PATH" echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build server (web) diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index c3565a9bf4b23..9b92bfcd08389 100644 --- a/build/azure-pipelines/cli/cli-compile.yml +++ b/build/azure-pipelines/cli/cli-compile.yml @@ -135,7 +135,7 @@ steps: env: CARGO_NET_GIT_FETCH_WITH_CLI: true VSCODE_CLI_COMMIT: $(Build.SourceVersion) - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" RUSTC_WRAPPER: sccache SCCACHE_DIR: $(Pipeline.Workspace)/sccache ${{ each pair in parameters.VSCODE_CLI_ENV }}: diff --git a/build/azure-pipelines/common/checkDistroCommit.ts b/build/azure-pipelines/common/checkDistroCommit.ts index 4003a10df317c..f6bf4657b1472 100644 --- a/build/azure-pipelines/common/checkDistroCommit.ts +++ b/build/azure-pipelines/common/checkDistroCommit.ts @@ -5,10 +5,16 @@ import path from 'path'; import fs from 'fs'; -import { retry } from './retry.ts'; +import { execSync } from 'child_process'; const root = path.dirname(path.dirname(path.dirname(import.meta.dirname))); +// The microsoft/vscode-distro repository is checked out locally by +// download-distro.yml (into .build/distro) using the agent's GitHub App +// (Monaco) credentials, so we can resolve branch heads without a token that +// has private repository access. +const distroPath = path.join(root, '.build', 'distro'); + function getEnv(name: string): string { const result = process.env[name]; @@ -19,30 +25,14 @@ function getEnv(name: string): string { return result; } -interface GitHubBranchResponse { - commit: { - sha: string; - }; -} - -async function getDistroBranchHead(branch: string, token: string): Promise { - const url = `https://api.github.com/repos/microsoft/vscode-distro/branches/${encodeURIComponent(branch)}`; - - const response = await fetch(url, { - headers: { - 'Accept': 'application/vnd.github+json', - 'Authorization': `Bearer ${token}`, - 'X-GitHub-Api-Version': '2022-11-28', - 'User-Agent': 'VSCode Build' - } - }); - - if (!response.ok) { - throw new Error(`Failed to fetch branch ${branch} from vscode-distro: ${response.status} ${response.statusText}`); +function assertDistroCheckout(): void { + if (!fs.existsSync(path.join(distroPath, '.git'))) { + throw new Error(`Expected a vscode-distro checkout at ${distroPath} but found none. Ensure download-distro.yml ran before this check.`); } +} - const data = await response.json() as GitHubBranchResponse; - return data.commit.sha; +function getDistroBranchHead(branch: string): string { + return execSync(`git -C "${distroPath}" rev-parse "refs/remotes/origin/${branch}"`, { encoding: 'utf8' }).trim(); } async function checkDistroCommit(): Promise { @@ -71,16 +61,18 @@ async function checkDistroCommit(): Promise { const branch = branchMatch[1]; console.log(`Current branch: ${branch}`); - // Get the GitHub token - const token = getEnv('GITHUB_TOKEN'); + // Make sure the distro repository is actually checked out before we try to + // resolve a branch head from it; otherwise a missing checkout would be + // indistinguishable from a branch that simply doesn't exist in distro. + assertDistroCheckout(); - // Fetch the HEAD of the matching branch in vscode-distro + // Resolve the HEAD of the matching branch from the local distro checkout let distroBranchHead: string; try { - distroBranchHead = await retry(() => getDistroBranchHead(branch, token)); + distroBranchHead = getDistroBranchHead(branch); } catch (error) { // If the branch doesn't exist in distro, that's expected for feature branches - console.log(`Could not fetch branch '${branch}' from vscode-distro: ${error}`); + console.log(`Could not resolve branch '${branch}' from local vscode-distro checkout: ${error}`); console.log('This is expected for feature branches that have not been merged to distro'); return; } diff --git a/build/azure-pipelines/common/checkout.yml b/build/azure-pipelines/common/checkout.yml index 427f190480a53..d1313b5e2ed6d 100644 --- a/build/azure-pipelines/common/checkout.yml +++ b/build/azure-pipelines/common/checkout.yml @@ -1,5 +1,10 @@ steps: + # Pin self to the default sources directory (`s`) so that adding a second + # checkout (e.g. the distro repository in download-distro.yml) does not relocate + # self into a repo-named subfolder, which would break every script that assumes + # self lives at $(Build.SourcesDirectory). - checkout: self + path: s fetchDepth: 1 fetchTags: false retryCountOnTaskFailure: 3 diff --git a/build/azure-pipelines/common/downloadNotice.ts b/build/azure-pipelines/common/downloadNotice.ts new file mode 100644 index 0000000000000..8c6aa297f2262 --- /dev/null +++ b/build/azure-pipelines/common/downloadNotice.ts @@ -0,0 +1,392 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Component Governance (CG) NOTICE cutover. +// +// During each platform compile stage this script pulls the CG-generated NOTICE +// (produced by the parallel Quality stage as the `notice_output` artifact) and +// overwrites the repo-root ThirdPartyNotices.txt BEFORE gulp packages it +// (build/gulpfile.vscode.ts reads 'ThirdPartyNotices.txt' at package time). +// +// It clones the proven copilot VSIX background-download harness: a detached +// `deemon` poller starts at compile start (maximum overlap) and the package +// step attaches/blocks on it right before packaging. If the artifact is ready +// by then the added wall-clock is near-zero; otherwise we block for at most a +// short budget and then DEGRADE to the legacy mixin notice. +// +// Design notes: +// * copy-then-overwrite: mixin-quality.ts is left untouched (shared chokepoint +// used by 8+ pipelines). The legacy ThirdPartyNotices.txt it lays down is +// our guaranteed fallback, so we never ship with NO notice. +// * NON-FATAL: this script always exits 0. A notice problem must never break +// packaging during the cutover. Steady-state can flip to fatal later. +// * SHORT poll budget (not the copilot 30min): a missing artifact must degrade +// to fallback fast, otherwise the non-fatal design is defeated by a 30min hang. +// * Three greppable markers so a build log answers "fresh vs stale vs off": +// [notice-cutover] RESULT=fresh overwrote from notice_output (logs producer) +// [notice-cutover] RESULT=fallback artifact missing -> kept legacy notice +// [notice-cutover] RESULT=disabled feature flag off -> kept legacy notice + +import fs from 'fs'; +import path from 'path'; +import { Readable } from 'stream'; +import type { ReadableStream } from 'stream/web'; +import { pipeline } from 'node:stream/promises'; +import yauzl from 'yauzl'; +import { e, requestAZDOAPI } from './publish.ts'; +import { retry } from './retry.ts'; + +const ARTIFACT_NAME = 'notice_output'; +const QUALITY_JOB_NAMES = ['Quality Checks', 'Quality']; +// The merged, shipping NOTICE (CG + scanned extension licenses + cglicenses overrides). +const SHIPPING_NOTICE_NAME = 'ThirdPartyNotices.new.txt'; +const TARGET_NOTICE = path.resolve('ThirdPartyNotices.txt'); + +// Poll budget: 30 attempts x 30s = 15 minutes. Deliberately far below the +// copilot 30min so a never-produced artifact degrades to fallback quickly, but +// with generous margin over the parallel Quality stage (CG + scan + merge). +// The gate accepts only once ThirdPartyNotices.new.txt has been downloaded, +// extracted, and validated non-empty (>1KB) -- merely seeing it in the +// container listing is NOT sufficient, because the listing entry can appear +// before the file's content has finished committing (see waitForNotice). On +// such a mid-upload miss we keep polling rather than falling back, so a +// fast-compiling platform can never ship the legacy notice while its peers +// ship the CG notice. The Quality-completion backstop ends the wait early only +// when .new.txt was never produced at all. +// NB: this is the OUTER artifact-availability poll. It is unrelated to the +// inner retry() wrapper in retry.ts, which independently retries each AZDO API +// call up to 10 times for transient network errors and emits no attempt log. +const POLL_ATTEMPTS = 30; +const POLL_INTERVAL_MS = 30_000; + +function log(message: string): void { + console.log(`[notice-cutover] [${new Date().toISOString()}] ${message}`); +} + +interface TimelineRecord { + readonly name: string; + readonly type: string; + readonly state: string; + readonly result: string; +} + +interface Timeline { + readonly records: TimelineRecord[]; +} + +// We need the container `data` field (which the publish.ts Artifact type omits) +// to list the FILES inside the notice_output container artifact. For a container +// artifact, resource.data looks like "#//". +interface NoticeArtifact { + readonly name: string; + readonly resource: { + readonly downloadUrl: string; + readonly data?: string; + readonly properties: { + readonly artifactsize: number; + }; + }; +} + +function installDiagnostics(): void { + process.on('uncaughtException', err => { + console.error('[notice-cutover] Uncaught exception:', err); + // Non-fatal: never break packaging because of a notice problem. + process.exit(0); + }); + process.on('unhandledRejection', reason => { + console.error('[notice-cutover] Unhandled rejection:', reason); + process.exit(0); + }); + for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'] as const) { + process.on(signal, () => { + console.error(`[notice-cutover] Received ${signal}, exiting.`); + process.exit(0); + }); + } +} + +function getAzdoFetchOptions() { + return { + headers: { + 'Accept': 'application/json;api-version=5.0-preview.1', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'en-US,en;q=0.9', + 'Referer': 'https://dev.azure.com', + Authorization: `Bearer ${e('SYSTEM_ACCESSTOKEN')}` + } + }; +} + +async function getPipelineArtifacts(): Promise { + const result = await requestAZDOAPI<{ readonly value: NoticeArtifact[] }>('artifacts'); + return result.value; +} + +// Lists the FILES inside a container artifact via the AZDO container-items API. +// NOTE: a file path can appear in this listing slightly BEFORE its content has +// finished committing to the container (observed in build 450517: the listing +// showed ThirdPartyNotices.new.txt one poll before the artifact download +// actually yielded it). So this listing is a NECESSARY but not SUFFICIENT +// readiness signal -- the caller must still download+extract+validate the file +// before accepting it. Returns the file paths, or undefined if the listing +// could not be retrieved (transient error) so the caller can distinguish "file +// genuinely absent" from "could not check". +async function listArtifactFiles(artifact: NoticeArtifact): Promise { + // resource.data for a container artifact looks like: #// + const match = /^#\/(\d+)\/(.+)$/.exec(artifact.resource.data ?? ''); + if (!match) { + return undefined; + } + + const [, containerId, itemPath] = match; + const collectionUri = e('SYSTEM_COLLECTIONURI').replace(/\/$/, ''); + const url = `${collectionUri}/_apis/resources/Containers/${containerId}?itemPath=${encodeURIComponent(itemPath)}&isShallow=false&api-version=4.1-preview.4`; + + const res = await retry(() => fetch(url, getAzdoFetchOptions())); + if (!res.ok) { + throw new Error(`Container items request failed: ${res.status}`); + } + + const body = await res.json() as { readonly value: { readonly path: string; readonly itemType: string }[] }; + return body.value.filter(item => item.itemType === 'file').map(item => item.path); +} + +async function getQualityJob(): Promise { + const timeline = await retry(() => requestAZDOAPI('timeline')); + return timeline.records.find(r => r.type === 'Job' && QUALITY_JOB_NAMES.includes(r.name)); +} + +async function downloadArtifact(artifact: NoticeArtifact, downloadPath: string): Promise { + const abortController = new AbortController(); + const timeout = setTimeout(() => abortController.abort(), 4 * 60 * 1000); + + try { + const res = await fetch(artifact.resource.downloadUrl, { ...getAzdoFetchOptions(), signal: abortController.signal }); + + if (!res.ok) { + throw new Error(`Unexpected status code: ${res.status}`); + } + + await pipeline(Readable.fromWeb(res.body as ReadableStream), fs.createWriteStream(downloadPath)); + } finally { + clearTimeout(timeout); + } +} + +async function unzip(zipPath: string, outputPath: string): Promise { + return new Promise((resolve, reject) => { + yauzl.open(zipPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => { + if (err) { + return reject(err); + } + + const result: string[] = []; + zipfile!.on('entry', entry => { + if (/\/$/.test(entry.fileName)) { + zipfile!.readEntry(); + } else { + zipfile!.openReadStream(entry, (err, istream) => { + if (err) { + return reject(err); + } + + const filePath = path.join(outputPath, entry.fileName); + + // Zip-slip guard: ensure the resolved entry path stays within + // outputPath. A crafted zip could use ../ segments to escape and + // overwrite repo files used later in the build. + const resolvedOutput = path.resolve(outputPath); + const resolvedTarget = path.resolve(filePath); + if (resolvedTarget !== resolvedOutput && !resolvedTarget.startsWith(resolvedOutput + path.sep)) { + return reject(new Error(`Zip entry escapes output directory (zip-slip): ${entry.fileName}`)); + } + + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + + const ostream = fs.createWriteStream(filePath); + ostream.on('finish', () => { + result.push(filePath); + zipfile!.readEntry(); + }); + ostream.on('error', err => reject(err)); + istream?.on('error', err => reject(err)); + istream!.pipe(ostream); + }); + } + }); + + zipfile!.on('close', () => resolve(result)); + zipfile!.readEntry(); + }); + }); +} + +interface ExtractedNotice { + readonly shippingPath: string; + readonly metaFile: string | undefined; + readonly size: number; +} + +// Downloads + extracts the notice_output artifact into tmpDir and validates that +// ThirdPartyNotices.new.txt is actually present AND non-empty (>1KB). Returns the +// extracted notice on success, or undefined if the file is missing/too small in +// the EXTRACTED output -- which, when the container listing claimed it was there, +// means the content is still committing (mid-upload). The caller treats undefined +// as "not ready yet" and re-polls; it must NEVER be treated as terminal fallback, +// because a fast platform that hits this window would otherwise ship the legacy +// notice while its peers ship the CG notice (the cross-arch mismatch bug). +async function tryExtractNotice(artifact: NoticeArtifact, tmpDir: string): Promise { + fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.mkdirSync(tmpDir, { recursive: true }); + const artifactZipPath = path.join(tmpDir, 'notice_output.zip'); + + log(' * downloading notice_output artifact to verify content...'); + await retry(() => downloadArtifact(artifact, artifactZipPath)); + + log(' * extracting notice_output artifact...'); + const files = await unzip(artifactZipPath, tmpDir); + const shipping = files.find(f => path.basename(f) === SHIPPING_NOTICE_NAME); + + if (!shipping) { + log(` * ${SHIPPING_NOTICE_NAME} listed but not yet in extracted output (mid-upload); will recheck next poll.`); + return undefined; + } + + const size = fs.statSync(shipping).size; + // Guard: a tiny file means the merge produced nothing usable OR the upload is + // still in flight. Either way, do not accept it -- re-poll. + if (size <= 1024) { + log(` * ${SHIPPING_NOTICE_NAME} extracted but too small (${size} bytes; mid-upload or empty merge); will recheck next poll.`); + return undefined; + } + + const metaFile = files.find(f => path.basename(f) === 'notice-meta.txt'); + return { shippingPath: shipping, metaFile, size }; +} + +// Returns the validated, extracted CG NOTICE if it becomes available within the +// short budget, otherwise undefined (caller keeps the legacy notice). +async function waitForNotice(tmpDir: string): Promise { + const startTime = Date.now(); + + for (let index = 0; index < POLL_ATTEMPTS; index++) { + const elapsed = Math.round((Date.now() - startTime) / 1000); + + try { + log(`Waiting for ${ARTIFACT_NAME} artifact (attempt ${index + 1}/${POLL_ATTEMPTS}, ${elapsed}s elapsed)...`); + + // The Quality job state is only a BACKSTOP (see below): if it has + // completed and .new.txt never even appeared in the listing, the file + // is never coming and we give up early instead of burning the budget. + const qualityJob = await getQualityJob().catch(() => undefined); + if (qualityJob) { + log(` * Quality job: state=${qualityJob.state}, result=${qualityJob.result ?? 'n/a'}`); + } + + const allArtifacts = await retry(() => getPipelineArtifacts()); + log(` * Found ${allArtifacts.length} artifact(s): ${allArtifacts.map(a => a.name).join(', ') || '(none)'}`); + + const artifact = allArtifacts.find(a => a.name === ARTIFACT_NAME); + if (artifact) { + // The notice_output container is populated in TWO phases by the + // Quality stage (first generated.txt + meta, then seconds-to-minutes + // later the shipping .new.txt). We accept ONLY once .new.txt has been + // downloaded, extracted, and validated non-empty -- the listing alone + // races against content-commit (build 450517 armhf), so on a listing + // hit we attempt a real download+extract and re-poll if it isn't ready. + const files = await listArtifactFiles(artifact).catch(() => undefined); + if (files === undefined) { + // Could not list (transient error or unexpected data shape). Do not + // give up; just retry next poll. The budget still bounds the wait. + log(' * could not list notice_output files yet; will recheck next poll'); + } else { + log(` * notice_output files: ${files.map(f => path.basename(f)).join(', ') || '(empty)'}`); + if (files.some(f => path.basename(f) === SHIPPING_NOTICE_NAME)) { + log(` * ${SHIPPING_NOTICE_NAME} listed; verifying extracted content...`); + const extracted = await tryExtractNotice(artifact, tmpDir).catch(err => { + // A download/extract failure here (e.g. partial zip mid-upload) + // is NOT terminal: keep polling within budget. + log(` * download/extract attempt failed (${err}); will recheck next poll.`); + return undefined; + }); + if (extracted) { + log(` * ${SHIPPING_NOTICE_NAME} extracted and validated (${extracted.size} bytes); accepting.`); + return extracted; + } + // Listed but not yet downloadable/valid: fall through to wait + re-poll. + // Deliberately NO early give-up here even if the Quality job reports + // "completed" -- the artifact upload is async (continueOnError) and may + // still be finalizing, so we let the budget bound the wait. + } else if (qualityJob && qualityJob.state === 'completed') { + // BACKSTOP: .new.txt was never even listed AND Quality finished + // => it is never coming. Fall back instead of polling the full budget. + log(` * Quality job completed but ${SHIPPING_NOTICE_NAME} never appeared in artifact; giving up.`); + return undefined; + } else { + log(` * ${SHIPPING_NOTICE_NAME} not in artifact yet; waiting...`); + } + } + } else if (qualityJob && qualityJob.state === 'completed') { + log(' * Quality job completed but no notice_output artifact; giving up.'); + return undefined; + } + + log(` * Not ready yet, waiting ${POLL_INTERVAL_MS / 1000}s...`); + } catch (err) { + console.error(`[notice-cutover] WARNING: poll attempt failed: ${err}`); + } + + await new Promise(c => setTimeout(c, POLL_INTERVAL_MS)); + } + + log(`Poll budget exhausted (${POLL_ATTEMPTS} attempts) without a valid ${SHIPPING_NOTICE_NAME}.`); + return undefined; +} + +async function main(): Promise { + installDiagnostics(); + + // Feature flag = instant rollback. Default ON during the cutover validation; + // set VSCODE_OVERWRITE_TPN=false to force the legacy mixin notice. Check is + // case-insensitive on purpose so YAML casing (true/false vs True/False) can't + // silently break the rollback lever. + if ((process.env['VSCODE_OVERWRITE_TPN'] ?? '').trim().toLowerCase() === 'false') { + log('RESULT=disabled feature flag off (VSCODE_OVERWRITE_TPN=false); keeping legacy notice.'); + return; + } + + const tmpDir = path.resolve('.build/tmp-notice'); + const notice = await waitForNotice(tmpDir); + if (!notice) { + // Only reached on GENUINE exhaustion or a confirmed never-produced artifact + // -- never on a transient mid-upload miss (those re-poll inside waitForNotice). + log(`RESULT=fallback ${SHIPPING_NOTICE_NAME} unavailable within budget; keeping legacy notice.`); + fs.rmSync(tmpDir, { recursive: true, force: true }); + return; + } + + // Log provenance for legal traceability: which build/commit produced this NOTICE. + if (notice.metaFile) { + log('NOTICE provenance:'); + for (const line of fs.readFileSync(notice.metaFile, 'utf8').split(/\r?\n/)) { + if (line.trim()) { + log(` ${line}`); + } + } + } + + const legacySize = fs.existsSync(TARGET_NOTICE) ? fs.statSync(TARGET_NOTICE).size : 0; + fs.copyFileSync(notice.shippingPath, TARGET_NOTICE); + log(`RESULT=fresh overwrote ${TARGET_NOTICE} with CG NOTICE (${notice.size} bytes; legacy was ${legacySize} bytes).`); + + fs.rmSync(tmpDir, { recursive: true, force: true }); +} + +main().catch(err => { + // Non-fatal: log and exit 0 so packaging proceeds with the legacy notice. + console.error('[notice-cutover] Non-fatal error; keeping legacy notice:', err); + process.exitCode = 0; +}); diff --git a/build/azure-pipelines/common/install-builtin-extensions.yml b/build/azure-pipelines/common/install-builtin-extensions.yml index f9cbfd4b0854a..d431bff7fc10e 100644 --- a/build/azure-pipelines/common/install-builtin-extensions.yml +++ b/build/azure-pipelines/common/install-builtin-extensions.yml @@ -19,6 +19,6 @@ steps: - script: node build/lib/builtInExtensions.ts env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" condition: and(succeeded(), ne(variables.BUILTIN_EXTENSIONS_RESTORED, 'true')) displayName: Download built-in extensions diff --git a/build/azure-pipelines/common/mixin-vscode-capi.yml b/build/azure-pipelines/common/mixin-vscode-capi.yml index 6acbd604ddd3a..4d2a42f619dc6 100644 --- a/build/azure-pipelines/common/mixin-vscode-capi.yml +++ b/build/azure-pipelines/common/mixin-vscode-capi.yml @@ -1,4 +1,13 @@ steps: + # Check out the private microsoft/vscode-capi repository using the agent's + # GitHub App (Monaco) credentials instead of cloning it with a PAT. The + # repository resource is declared in the top-level pipeline (resources.repositories). + - checkout: capi + path: s/.build/vscode-capi + fetchDepth: 1 + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vscode-capi + - pwsh: | $ErrorActionPreference = 'Stop' @@ -10,25 +19,14 @@ steps: } } - $CapiPath = Join-Path '$(Agent.BuildDirectory)' 'vscode-capi' - if (Test-Path $CapiPath) { - Remove-Item -Recurse -Force $CapiPath - } + $CapiPath = Join-Path '$(Build.SourcesDirectory)' '.build/vscode-capi' + Push-Location $CapiPath try { - Invoke-CheckedCommand { git clone https://github.com/microsoft/vscode-capi.git --depth 1 $CapiPath } - Push-Location $CapiPath - - try { - Invoke-CheckedCommand { npm ci } - $env:BUILD_SOURCESDIRECTORY = '$(Build.SourcesDirectory)' - Invoke-CheckedCommand { npm run mixin_vscode } - } finally { - Pop-Location - } + Invoke-CheckedCommand { npm ci } + $env:BUILD_SOURCESDIRECTORY = '$(Build.SourcesDirectory)' + Invoke-CheckedCommand { npm run mixin_vscode } } finally { - if (Test-Path $CapiPath) { - Remove-Item -Recurse -Force $CapiPath - } + Pop-Location } displayName: Mixin vscode-capi diff --git a/build/azure-pipelines/copilot/build-steps.yml b/build/azure-pipelines/copilot/build-steps.yml index 6a0ae05d86d11..4e4d8afd87ded 100644 --- a/build/azure-pipelines/copilot/build-steps.yml +++ b/build/azure-pipelines/copilot/build-steps.yml @@ -1,10 +1,18 @@ steps: + # Check out the private microsoft/vscode-capi repository using the agent's + # GitHub App (Monaco) credentials instead of cloning it with a PAT. The + # repository resource is declared in the top-level pipeline (resources.repositories). + - checkout: capi + path: s/.build/vscode-capi + fetchDepth: 1 + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vscode-capi + - script: | set -e - git clone https://github.com/microsoft/vscode-capi.git --depth 1 $(Agent.BuildDirectory)/vscode-capi - cd $(Agent.BuildDirectory)/vscode-capi - npm ci && BUILD_SOURCESDIRECTORY=$(Build.SourcesDirectory)/extensions/copilot npm run mixin - rm -rf $(Agent.BuildDirectory)/vscode-capi + cd $(Build.SourcesDirectory)/.build/vscode-capi + npm ci + BUILD_SOURCESDIRECTORY=$(Build.SourcesDirectory)/extensions/copilot npm run mixin displayName: Mixin vscode-capi - script: npm run build diff --git a/build/azure-pipelines/copilot/l10n-steps.yml b/build/azure-pipelines/copilot/l10n-steps.yml index 99d5b249d6fc8..a7857a69c8afe 100644 --- a/build/azure-pipelines/copilot/l10n-steps.yml +++ b/build/azure-pipelines/copilot/l10n-steps.yml @@ -1,21 +1,23 @@ steps: + # Check out the private microsoft/vscode-extensions-loc repository using the + # agent's GitHub App (Monaco) credentials instead of cloning it with a PAT. + # The repository resource is declared in the top-level pipeline (resources.repositories). + - checkout: vscode_loc + path: s/.build/vscode-extensions-loc + fetchDepth: 1 + sparseCheckoutDirectories: out/GitHub.copilot-chat + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vscode-extensions-loc + - script: | set -e EXTENSION_ID="GitHub.copilot-chat" - L10N_REPO="https://github.com/microsoft/vscode-extensions-loc.git" - L10N_DIR="$(Agent.TempDirectory)/vscode-extensions-loc" - - echo "Cloning vscode-extensions-loc (sparse checkout)..." - git clone --depth 1 --filter=blob:none --sparse "$L10N_REPO" "$L10N_DIR" - cd "$L10N_DIR" - git sparse-checkout set "out/$EXTENSION_ID" - + L10N_DIR="$(Build.SourcesDirectory)/.build/vscode-extensions-loc" TRANSLATED_DIR="$L10N_DIR/out/$EXTENSION_ID" if [ ! -d "$TRANSLATED_DIR" ] || [ -z "$(ls -A "$TRANSLATED_DIR" 2>/dev/null)" ]; then echo "No translated strings found for $EXTENSION_ID, skipping l10n import." - rm -rf "$L10N_DIR" exit 0 fi @@ -36,7 +38,4 @@ steps: echo "Localized files:" ls -la package.nls.*.json 2>/dev/null || echo " (no package.nls.*.json)" ls -la "$L10N_ROOT"/bundle.l10n.*.json 2>/dev/null || echo " (no bundle.l10n.*.json)" - - # Cleanup - rm -rf "$L10N_DIR" displayName: Import localized strings diff --git a/build/azure-pipelines/copilot/setup-steps.yml b/build/azure-pipelines/copilot/setup-steps.yml index 83699f8152d63..e550d6ac69605 100644 --- a/build/azure-pipelines/copilot/setup-steps.yml +++ b/build/azure-pipelines/copilot/setup-steps.yml @@ -3,28 +3,6 @@ steps: inputs: versionSpec: "22.21.x" - - task: AzureKeyVault@2 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - pwsh: | - "machine github.com`nlogin vscode`npassword $(github-distro-mixin-password)" | Out-File "$Home/_netrc" -Encoding ASCII - condition: and(succeeded(), contains(variables['Agent.OS'], 'windows')) - displayName: Setup distro auth (Windows) - - - script: | - mkdir -p .build - cat << EOF | tee ~/.netrc .build/.netrc > /dev/null - machine github.com - login vscode - password $(github-distro-mixin-password) - EOF - condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) - displayName: Setup distro auth (non-Windows) - - pwsh: node build/setup-npm-registry.ts $env:NPM_REGISTRY workingDirectory: $(Build.SourcesDirectory) condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none'), contains(variables['Agent.OS'], 'windows')) diff --git a/build/azure-pipelines/darwin/product-build-darwin-cli.yml b/build/azure-pipelines/darwin/product-build-darwin-cli.yml index 5a705b43b8ef2..1d68fe9161348 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli.yml @@ -38,6 +38,13 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub token" + inputs: + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" + - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 diff --git a/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml b/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml index d70adbee0af01..e811c7397a72c 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml @@ -28,7 +28,14 @@ jobs: inputs: azureSubscription: vscode KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password,macos-developer-certificate,macos-developer-certificate-key" + SecretsFilter: "macos-developer-certificate,macos-developer-certificate-key" + + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub token" + inputs: + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -78,7 +85,7 @@ jobs: npm_config_arch: $(VSCODE_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 # flipped the default to support legacy linux distros which shouldn't happen diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index c5e4dfb588a84..455adf5e2cf58 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -36,7 +36,14 @@ jobs: inputs: azureSubscription: vscode KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password,macos-developer-certificate,macos-developer-certificate-key" + SecretsFilter: "macos-developer-certificate,macos-developer-certificate-key" + + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub token" + inputs: + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: node build/setup-npm-registry.ts $NPM_REGISTRY build condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -71,7 +78,7 @@ jobs: done workingDirectory: build env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install build dependencies - pwsh: node -- build/azure-pipelines/common/waitForArtifacts.ts unsigned_vscode_client_darwin_x64_archive unsigned_vscode_client_darwin_arm64_archive diff --git a/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml b/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml new file mode 100644 index 0000000000000..9450451b0129a --- /dev/null +++ b/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml @@ -0,0 +1,67 @@ +parameters: + - name: iterations + type: object + +jobs: + - job: macOSSmokeFlaky + displayName: macOS (Electron) + timeoutInMinutes: 300 + variables: + VSCODE_ARCH: arm64 + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/crashes + artifactName: crash-dump-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Crash Reports + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/logs + artifactName: logs-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Log Files + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + steps: + # Build VS Code from source. No VSCODE_RUN_*_TESTS flags are passed, so the + # compile template builds the client/server but skips compiling the test + # suites and skips the unit/integration/smoke test template entirely — we + # run only the smoke tests below, nothing else. + - template: ./steps/product-build-darwin-compile.yml@self + parameters: + VSCODE_ARCH: arm64 + VSCODE_CIBUILD: true + + - script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" + env: + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + displayName: Download Electron and Playwright + retryCountOnTaskFailure: 3 + + # The compile template only compiles the smoke suite when a test flag is + # set; since we pass none, compile it explicitly here. + - script: npm run compile --prefix test/smoke + displayName: Compile smoke tests + + # Guarantee the artifact directories exist so the 1ES output publishing + # never fails on a missing path. The smoke runner writes logs under + # .build/logs and crash dumps under .build/crashes (only on a crash). + - script: mkdir -p .build/crashes .build/logs + displayName: Ensure artifact directories exist + + # Run the Electron smoke test once per iteration. continueOnError lets every + # iteration run even if some fail, and gives each run its own timeline record + # so the SmokeFlaky function can tally passes vs. failures. + - ${{ each i in parameters.iterations }}: + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + npm run smoketest-no-compile -- --tracing --build "$APP_ROOT/$APP_NAME" + displayName: "🧪 Smoke test iteration ${{ i }}/${{ length(parameters.iterations) }} (Electron)" + continueOnError: true + timeoutInMinutes: 20 diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml index b7315e170ee62..20aa65bd40733 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml @@ -28,7 +28,14 @@ steps: inputs: azureSubscription: vscode KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password,macos-developer-certificate,macos-developer-certificate-key" + SecretsFilter: "macos-developer-certificate,macos-developer-certificate-key" + + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub token" + inputs: + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -82,7 +89,7 @@ steps: npm_config_arch: $(VSCODE_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 # flipped the default to support legacy linux distros which shouldn't happen @@ -112,6 +119,16 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality + # Start pulling the CG-generated NOTICE from the parallel Quality stage + # in the background (overwrites repo-root ThirdPartyNotices.txt before packaging). + # Publish builds only (CIBUILD=false) — CI test jobs never ship a notice. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - script: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Pull CG NOTICE (background) + - template: ../../common/install-builtin-extensions.yml@self - template: ../../common/agent-sdk-produce.yml@self @@ -127,7 +144,7 @@ steps: - script: npm run gulp core-ci env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Compile - script: node build/azure-pipelines/common/extract-telemetry.ts @@ -151,12 +168,25 @@ steps: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Download Copilot VSIX + # Block on the CG NOTICE download and overwrite ThirdPartyNotices.txt + # before gulp packages it (and before any later signing/notarization). + # Non-fatal: falls back to legacy on any problem. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - script: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Apply CG NOTICE + # deemon --attach can exit non-zero if the background daemon died; never fail for that. + continueOnError: true + - script: | set -e npm run gulp vscode-darwin-$(VSCODE_ARCH)-min-ci echo "##vso[task.setvariable variable=BUILT_CLIENT]true" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build client - script: | @@ -164,7 +194,8 @@ steps: npm run gulp vscode-reh-darwin-$(VSCODE_ARCH)-min-ci mv ../vscode-reh-darwin-$(VSCODE_ARCH) ../vscode-server-darwin-$(VSCODE_ARCH) # TODO@joaomoreno env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build server - script: | @@ -172,7 +203,8 @@ steps: npm run gulp vscode-reh-web-darwin-$(VSCODE_ARCH)-min-ci mv ../vscode-reh-web-darwin-$(VSCODE_ARCH) ../vscode-server-darwin-$(VSCODE_ARCH)-web # TODO@joaomoreno env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build server (web) - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-test.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-test.yml index 2cb75de8dddd6..e790f126a7729 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-test.yml @@ -9,7 +9,8 @@ parameters: steps: - script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Download Electron and Playwright retryCountOnTaskFailure: 3 diff --git a/build/azure-pipelines/dependencies-check.yml b/build/azure-pipelines/dependencies-check.yml index d7d0d9f12b6dd..78b74bc342523 100644 --- a/build/azure-pipelines/dependencies-check.yml +++ b/build/azure-pipelines/dependencies-check.yml @@ -27,6 +27,13 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub token" + inputs: + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" + - script: | set -e echo "Checking if package.json or package-lock.json files were modified..." @@ -102,7 +109,7 @@ jobs: npm_command: 'install --ignore-scripts' ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install dependencies with retries timeoutInMinutes: 1300 condition: and(succeeded(), eq(variables['SHOULD_VALIDATE'], 'true')) diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index 1d0a50b129778..05ac00cec7db8 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -7,7 +7,35 @@ trigger: include: ["main", "release/*"] pr: none +resources: + repositories: + - repository: distro + type: github + name: microsoft/vscode-distro + ref: main + endpoint: Monaco + - repository: encrypt + type: github + name: microsoft/vscode-encrypt + ref: main + endpoint: Monaco + - repository: vsda + type: github + name: microsoft/vsda + ref: main + endpoint: Monaco + - repository: regexp + type: github + name: microsoft/vscode-regexp-languagedetection + ref: main + endpoint: Monaco + steps: + - checkout: self + path: s + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vscode + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/distro/download-distro.yml b/build/azure-pipelines/distro/download-distro.yml index b7c52dd36b41c..7d6319ee0be73 100644 --- a/build/azure-pipelines/distro/download-distro.yml +++ b/build/azure-pipelines/distro/download-distro.yml @@ -1,61 +1,90 @@ steps: - - task: AzureKeyVault@2 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - # TODO@joaomoreno: Keep pwsh once we move out of running entire jobs in containers - - pwsh: | - "machine github.com`nlogin vscode`npassword $(github-distro-mixin-password)" | Out-File "$Home/_netrc" -Encoding ASCII - condition: and(succeeded(), contains(variables['Agent.OS'], 'windows')) - displayName: Setup distro auth (Windows) + # Check out the private microsoft/vscode-distro repository using the agent's + # GitHub App (Monaco) credentials. This avoids the need for a long-lived PAT. + # The repository resource is declared in the top-level pipeline (resources.repositories) + # and is checked out into .build/distro so that mixin-npm.ts / mixin-quality.ts can + # consume it from there (`.build/distro/npm`, `.build/distro/mixin/`). + - checkout: distro + path: s/.build/distro + fetchDepth: 0 + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vscode-distro - pwsh: | $ErrorActionPreference = "Stop" - $ArchivePath = "$(Agent.TempDirectory)/distro.zip" - $PackageJson = Get-Content -Path package.json -Raw | ConvertFrom-Json - $DistroVersion = $PackageJson.distro - - Invoke-WebRequest -Uri "https://api.github.com/repos/microsoft/vscode-distro/zipball/$DistroVersion" ` - -OutFile $ArchivePath ` - -Headers @{ "Accept" = "application/vnd.github+json"; "Authorization" = "Bearer $(github-distro-mixin-password)"; "X-GitHub-Api-Version" = "2022-11-28" } + $DistroVersion = (Get-Content -Path package.json -Raw | ConvertFrom-Json).distro + Write-Host "Checking out distro commit $DistroVersion" + git -C .build/distro checkout $DistroVersion + displayName: Checkout distro commit - New-Item -ItemType Directory -Path .build -Force - Expand-Archive -Path $ArchivePath -DestinationPath .build - Rename-Item -Path ".build/microsoft-vscode-distro-$DistroVersion" -NewName distro - condition: and(succeeded(), contains(variables['Agent.OS'], 'windows')) - displayName: Download distro (Windows) + - script: git lfs install --local + displayName: Initialize Git LFS - - script: | - mkdir -p .build - cat << EOF | tee ~/.netrc .build/.netrc > /dev/null - machine github.com - login vscode - password $(github-distro-mixin-password) - EOF - condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) - displayName: Setup distro auth (non-Windows) + - script: git lfs pull + displayName: Pull Git LFS objects - - script: | - set -e - ArchivePath="$(Agent.TempDirectory)/distro.zip" - DistroVersion=$(node -p "require('./package.json').distro") + # Check out the private microsoft/vscode-encrypt, microsoft/vsda and + # microsoft/vscode-regexp-languagedetection repositories using the agent's GitHub + # App (Monaco) credentials. The distro's npm dependencies reference vsda and + # vscode-regexp-languagedetection as git dependencies, and the CLI cargo patches + # reference vsda and vscode-encrypt; npm resolves these over ssh while cargo + # resolves them over https. We redirect both public GitHub URLs to these local + # checkouts via git's insteadOf so no PAT is required to resolve them. + - checkout: encrypt + path: s/.build/vscode-encrypt + fetchDepth: 0 + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vscode-encrypt - curl -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $(github-distro-mixin-password)" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - -o $ArchivePath \ - -L "https://api.github.com/repos/microsoft/vscode-distro/zipball/$DistroVersion" + - checkout: vsda + path: s/.build/vsda + fetchDepth: 0 + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vsda - unzip $ArchivePath -d .build - mv .build/microsoft-vscode-distro-$DistroVersion .build/distro - condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) - displayName: Download distro (non-Windows) + - checkout: regexp + path: s/.build/vscode-regexp-languagedetection + fetchDepth: 0 + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vscode-regexp-languagedetection - - script: git lfs install --local - displayName: Initialize Git LFS + # Redirect the private git dependencies to the local checkouts above, so no PAT + # is required to resolve them. npm resolves these over ssh while cargo resolves + # them over https, with and without a trailing `.git`, so we register every + # form. This step runs on the agent directly and must work in both bash and + # cmd.exe (Windows), so it uses plain `git config` invocations. + - script: | + git config --global url."file://$(Build.SourcesDirectory)/.build/vscode-encrypt".insteadOf "https://github.com/microsoft/vscode-encrypt" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vscode-encrypt".insteadOf "https://github.com/microsoft/vscode-encrypt.git" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vscode-encrypt".insteadOf "ssh://git@github.com/microsoft/vscode-encrypt" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vscode-encrypt".insteadOf "ssh://git@github.com/microsoft/vscode-encrypt.git" + git config --global url."file://$(Build.SourcesDirectory)/.build/vsda".insteadOf "https://github.com/microsoft/vsda" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vsda".insteadOf "https://github.com/microsoft/vsda.git" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vsda".insteadOf "ssh://git@github.com/microsoft/vsda" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vsda".insteadOf "ssh://git@github.com/microsoft/vsda.git" + git config --global url."file://$(Build.SourcesDirectory)/.build/vscode-regexp-languagedetection".insteadOf "https://github.com/microsoft/vscode-regexp-languagedetection" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vscode-regexp-languagedetection".insteadOf "https://github.com/microsoft/vscode-regexp-languagedetection.git" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vscode-regexp-languagedetection".insteadOf "ssh://git@github.com/microsoft/vscode-regexp-languagedetection" + git config --global --add url."file://$(Build.SourcesDirectory)/.build/vscode-regexp-languagedetection".insteadOf "ssh://git@github.com/microsoft/vscode-regexp-languagedetection.git" + displayName: Redirect private git dependencies to local checkouts - - script: git lfs pull - displayName: Pull Git LFS objects + # The Alpine server build installs the distro's npm dependencies inside a docker + # container that mounts the sources at /root/vscode, so the host redirects above + # (which use agent paths) do not apply there. Emit a container-pathed gitconfig + # that postinstall.ts bind-mounts as /root/.gitconfig. Only needed (and only + # bash-compatible) on Linux agents. + - script: | + set -e + CONTAINER_GITCONFIG="$(Build.SourcesDirectory)/.build/.gitconfig-distro" + rm -f "$CONTAINER_GITCONFIG" + for repo in vscode-encrypt vsda vscode-regexp-languagedetection; do + for url in \ + "https://github.com/microsoft/$repo" \ + "https://github.com/microsoft/$repo.git" \ + "ssh://git@github.com/microsoft/$repo" \ + "ssh://git@github.com/microsoft/$repo.git"; do + git config --file "$CONTAINER_GITCONFIG" --add url."file:///root/vscode/.build/$repo".insteadOf "$url" + done + done + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + displayName: Write container git redirects for Alpine diff --git a/build/azure-pipelines/linux/product-build-linux-cli.yml b/build/azure-pipelines/linux/product-build-linux-cli.yml index a9107129b73b5..c84a1fd08e273 100644 --- a/build/azure-pipelines/linux/product-build-linux-cli.yml +++ b/build/azure-pipelines/linux/product-build-linux-cli.yml @@ -34,6 +34,13 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub token" + inputs: + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" + - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 @@ -84,7 +91,7 @@ jobs: done workingDirectory: build env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install build dependencies - script: | diff --git a/build/azure-pipelines/linux/product-build-linux-node-modules.yml b/build/azure-pipelines/linux/product-build-linux-node-modules.yml index ad0e149816014..9be0c3f1daf40 100644 --- a/build/azure-pipelines/linux/product-build-linux-node-modules.yml +++ b/build/azure-pipelines/linux/product-build-linux-node-modules.yml @@ -27,9 +27,9 @@ jobs: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: | set -e @@ -93,7 +93,7 @@ jobs: done workingDirectory: build env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -109,7 +109,7 @@ jobs: SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' env: VSCODE_ARCH: $(VSCODE_ARCH) - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Download vscode sysroots condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -135,7 +135,7 @@ jobs: VSCODE_ARCH: $(VSCODE_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/linux/product-smoke-flaky-linux.yml b/build/azure-pipelines/linux/product-smoke-flaky-linux.yml new file mode 100644 index 0000000000000..b3a7e11ded72a --- /dev/null +++ b/build/azure-pipelines/linux/product-smoke-flaky-linux.yml @@ -0,0 +1,82 @@ +parameters: + - name: VSCODE_QUALITY + type: string + - name: iterations + type: object + +jobs: + - job: LinuxSmokeFlaky + displayName: Linux (Electron) + timeoutInMinutes: 300 + variables: + DISPLAY: ":10" + NPM_ARCH: x64 + VSCODE_ARCH: x64 + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/crashes + artifactName: crash-dump-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Crash Reports + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/logs + artifactName: logs-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Log Files + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + steps: + # Build VS Code from source. No VSCODE_RUN_*_TESTS flags are passed, so the + # compile template builds the client/server but skips compiling the test + # suites and skips the unit/integration/smoke test template entirely — we + # run only the smoke tests below, nothing else. + - template: ./steps/product-build-linux-compile.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_CIBUILD: true + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + + - script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" + env: + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + displayName: Download Electron and Playwright + retryCountOnTaskFailure: 3 + + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + ELECTRON_ROOT=.build/electron + sudo chown root $APP_ROOT/chrome-sandbox + sudo chown root $ELECTRON_ROOT/chrome-sandbox + sudo chmod 4755 $APP_ROOT/chrome-sandbox + sudo chmod 4755 $ELECTRON_ROOT/chrome-sandbox + displayName: Change setuid helper binary permission + + # The compile template only compiles the smoke suite when a test flag is + # set; since we pass none, compile it explicitly here. + - script: npm run compile --prefix test/smoke + displayName: Compile smoke tests + + # Guarantee the artifact directories exist so the 1ES output publishing + # never fails on a missing path. The smoke runner writes logs under + # .build/logs and crash dumps under .build/crashes (only on a crash). + - script: mkdir -p .build/crashes .build/logs + displayName: Ensure artifact directories exist + + # Run the Electron smoke test once per iteration. continueOnError lets every + # iteration run even if some fail, and gives each run its own timeline record + # so the SmokeFlaky function can tally passes vs. failures. + - ${{ each i in parameters.iterations }}: + - script: | + set -e + npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" + env: + TMPDIR: $(Agent.TempDirectory) + displayName: "🧪 Smoke test iteration ${{ i }}/${{ length(parameters.iterations) }} (Electron)" + continueOnError: true + timeoutInMinutes: 20 diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index 2f275d1597581..b9dc1d80b995e 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -39,7 +39,7 @@ EOF if [ "$npm_config_arch" == "x64" ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/148.0.7778.97/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/148.0.7778.271/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux # Download libcxx headers and objects from upstream electron releases DEBUG=libcxx-fetcher \ @@ -51,9 +51,9 @@ if [ "$npm_config_arch" == "x64" ]; then # Set compiler toolchain # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:build/config/c++/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.271:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.271:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.271:build/config/c++/BUILD.gn export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu" export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu" export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" diff --git a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml index 4443e0cd869cc..7c60614f64ce6 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml @@ -34,9 +34,9 @@ steps: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: | set -e @@ -104,7 +104,7 @@ steps: done workingDirectory: build env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -120,7 +120,7 @@ steps: SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' env: VSCODE_ARCH: $(VSCODE_ARCH) - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Download vscode sysroots - script: | @@ -145,7 +145,7 @@ steps: VSCODE_ARCH: $(VSCODE_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -170,6 +170,16 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality + # Start pulling the CG-generated NOTICE from the parallel Quality stage + # in the background (overwrites repo-root ThirdPartyNotices.txt before packaging). + # Publish builds only (CIBUILD=false) — CI test jobs never ship a notice. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - script: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Pull CG NOTICE (background) + - template: ../../common/install-builtin-extensions.yml@self - ${{ if ne(parameters.VSCODE_ARCH, 'armhf') }}: @@ -186,7 +196,7 @@ steps: - script: npm run gulp core-ci env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Compile - ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}: @@ -211,6 +221,17 @@ steps: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Download Copilot VSIX + # Block on the CG NOTICE download and overwrite ThirdPartyNotices.txt + # right before gulp packages it. Non-fatal: falls back to legacy on any problem. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - script: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Apply CG NOTICE + # deemon --attach can exit non-zero if the background daemon died; never fail for that. + continueOnError: true + - script: | set -e npm run gulp vscode-linux-$(VSCODE_ARCH)-min-ci @@ -219,7 +240,8 @@ steps: echo "##vso[task.setvariable variable=CLIENT_PATH]$ARCHIVE_PATH" echo "##vso[task.setvariable variable=CLIENT_ARCHIVE_NAME]$(basename $ARCHIVE_PATH)" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build client - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: @@ -247,7 +269,7 @@ steps: set -e tar -czf $CLIENT_PATH -C .. VSCode-linux-$(VSCODE_ARCH) env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Archive client - ${{ if ne(parameters.VSCODE_ARCH, 'armhf') }}: @@ -262,7 +284,8 @@ steps: echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" echo "##vso[task.setvariable variable=SERVER_UNARCHIVE_PATH]$UNARCHIVE_PATH" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build server - script: | @@ -274,7 +297,8 @@ steps: tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH)-web echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build server (web) - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: @@ -296,7 +320,7 @@ steps: set -e npm run gulp "vscode-linux-$(VSCODE_ARCH)-prepare-deb" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Prepare deb package - script: | diff --git a/build/azure-pipelines/linux/steps/product-build-linux-test.yml b/build/azure-pipelines/linux/steps/product-build-linux-test.yml index 5c6223cf371d8..57b712fe1c349 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-test.yml @@ -9,7 +9,8 @@ parameters: steps: - script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Download Electron and Playwright retryCountOnTaskFailure: 3 @@ -109,10 +110,23 @@ steps: - script: | set -e + df -h + # Break down what is occupying the small OS root disk (`/`). The `-x` flag + # keeps `du` on the root filesystem so it does not descend into the large + # `/mnt` work volume. This identifies the directory that fills `/` and + # crashes the agent worker with "No space left on device". + echo "--- top consumers on / ---" + du -xhd1 / 2>/dev/null | sort -rh | head -30 || true + echo "--- top consumers under \$HOME ---" + du -xhd1 "$HOME" 2>/dev/null | sort -rh | head -30 || true + echo "--- top consumers under /tmp ---" + du -xhd1 /tmp 2>/dev/null | sort -rh | head -30 || true + echo "--- top consumers under /var ---" + du -xhd1 /var 2>/dev/null | sort -rh | head -30 || true ps -ef cat /proc/sys/fs/inotify/max_user_watches lsof | wc -l - displayName: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) + displayName: Diagnostics before smoke test run (disk space, processes, max_user_watches, number of opened file handles) continueOnError: true condition: succeededOrFailed() @@ -120,6 +134,12 @@ steps: - script: | set -e npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" + env: + # Keep the smoke test data dir (user-data, extensions, workspace and the + # Copilot CLI session state under `os.tmpdir()`) on the large work volume + # instead of the small OS root disk (`/tmp`), which has filled up and + # crashed the agent worker with "No space left on device". + TMPDIR: $(Agent.TempDirectory) timeoutInMinutes: 20 displayName: 🧪 Run smoke tests (Electron) @@ -127,6 +147,7 @@ steps: - script: npm run smoketest-no-compile -- --web --tracing --headless env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)-web + TMPDIR: $(Agent.TempDirectory) timeoutInMinutes: 20 displayName: 🧪 Run smoke tests (Browser, Chromium) @@ -136,15 +157,26 @@ steps: APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)" \ npm run smoketest-no-compile -- --tracing --remote --build "$APP_PATH" + env: + TMPDIR: $(Agent.TempDirectory) timeoutInMinutes: 20 displayName: 🧪 Run smoke tests (Remote) - script: | set -e + df -h + # Same root-disk breakdown as the "before" step so the delta attributable to + # the smoke run itself is visible, plus the smoke test data dirs explicitly. + echo "--- top consumers on / ---" + du -xhd1 / 2>/dev/null | sort -rh | head -30 || true + echo "--- top consumers under \$HOME ---" + du -xhd1 "$HOME" 2>/dev/null | sort -rh | head -30 || true + echo "--- smoke test data dirs ---" + du -sh "$(Agent.TempDirectory)"/vscsmoke-* /tmp/vscsmoke-* 2>/dev/null || true ps -ef cat /proc/sys/fs/inotify/max_user_watches lsof | wc -l - displayName: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) + displayName: Diagnostics after smoke test run (disk space, processes, max_user_watches, number of opened file handles) continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/oss/merge-notices.test.ts b/build/azure-pipelines/oss/merge-notices.test.ts new file mode 100644 index 0000000000000..33687f699242b --- /dev/null +++ b/build/azure-pipelines/oss/merge-notices.test.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/*--------------------------------------------------------------------------------------------- + * Focused unit checks for computeUnaccounted() in merge-notices.ts. + * + * Run: npx tsx merge-notices.test.ts + * + * computeUnaccounted cross-checks the scanner's "unresolved" list against the + * FINAL merged NOTICE to surface packages genuinely missing from it -- either + * (a) the scanner failed to create a row AND nothing rescued it downstream, or + * (b) a row exists but its license body is empty. Covers: + * 1. A genuinely-absent unresolved pkg is included. + * 2. An unresolved pkg that IS present in `merged` (any version) is excluded + * -- the linux-keyutils cglicenses.json-override rescue case. + * 3. An entry present with empty licenseText is included (no-license-text). + * 4. An entry present with real license text is excluded. + * 5. Dedupe when the same pkg surfaces from both sources. + *--------------------------------------------------------------------------------------------*/ + +import { computeUnaccounted } from './merge-notices.js'; + +interface NoticeEntry { + name: string; + version: string; + license: string; + url: string; + licenseText: string; +} + +let passed = 0; +let failed = 0; + +function check(name: string, cond: boolean): void { + if (cond) { + passed++; + console.log(` ok ${name}`); + } else { + failed++; + console.error(` FAIL ${name}`); + } +} + +function entry(name: string, version: string, licenseText: string): NoticeEntry { + return { name, version, license: 'MIT', url: '', licenseText }; +} + +function mapOf(...entries: NoticeEntry[]): Map { + const m = new Map(); + for (const e of entries) { + m.set(`${e.name.toLowerCase()}@${e.version || ''}`, e); + } + return m; +} + +// -- 1. genuinely-absent unresolved pkg is included --------------------------- +console.log('computeUnaccounted -- genuinely absent:'); +{ + const merged = mapOf(entry('present-pkg', '1.0.0', 'Real license text')); + const unresolved = [{ name: 'ghost-pkg', version: '2.0.0', reason: 'cargo-no-license-resolved' }]; + const out = computeUnaccounted(merged, unresolved); + check('absent unresolved pkg is listed', out.some(u => u.name === 'ghost-pkg' && u.reason === 'cargo-no-license-resolved')); + check('present pkg with text is NOT listed', !out.some(u => u.name === 'present-pkg')); + check('exactly one unaccounted', out.length === 1); +} + +// -- 2. unresolved pkg present in merged (any version) is excluded ------------- +console.log('computeUnaccounted -- downstream-rescued (linux-keyutils case):'); +{ + // Scanner failed at 0.2.4, but cglicenses.json injected it at a different + // version. Name-only match must exclude it from the unaccounted list. + const merged = mapOf(entry('linux-keyutils', '0.2.5', 'BSD-3-Clause text')); + const unresolved = [{ name: 'linux-keyutils', version: '0.2.4', reason: 'cargo-no-license-resolved' }]; + const out = computeUnaccounted(merged, unresolved); + check('rescued pkg (version drift) is excluded', !out.some(u => u.name === 'linux-keyutils')); + check('nothing unaccounted', out.length === 0); +} +{ + // Exact-version rescue too. + const merged = mapOf(entry('linux-keyutils', '0.2.4', 'BSD-3-Clause text')); + const unresolved = [{ name: 'linux-keyutils', version: '0.2.4', reason: 'cargo-no-license-resolved' }]; + const out = computeUnaccounted(merged, unresolved); + check('rescued pkg (same version) is excluded', out.length === 0); +} + +// -- 3. present entry with empty license body is included ---------------------- +console.log('computeUnaccounted -- empty license body:'); +{ + const merged = mapOf( + entry('blank-pkg', '1.0.0', ' \n\t '), + entry('good-pkg', '1.0.0', 'Real text'), + ); + const out = computeUnaccounted(merged, []); + check('empty-body entry listed as no-license-text', out.some(u => u.name === 'blank-pkg' && u.reason === 'no-license-text')); + check('real-text entry excluded', !out.some(u => u.name === 'good-pkg')); + check('exactly one unaccounted', out.length === 1); +} + +// -- 4. dedupe between the two sources ---------------------------------------- +console.log('computeUnaccounted -- dedupe across sources:'); +{ + // Same pkg is both in unresolved AND present-with-empty-body. (Edge case, but + // the dedupe must keep a single row keyed by name@version.) + const merged = mapOf(entry('dup-pkg', '3.1.4', '')); + const unresolved = [{ name: 'dup-pkg', version: '3.1.4', reason: 'cargo-api-failed' }]; + const out = computeUnaccounted(merged, unresolved); + // Present in merged, so the unresolved branch (name-match) excludes it; the + // empty-body branch adds it once. Either way: exactly one row, not two. + check('single deduped row for dup-pkg', out.filter(u => u.name === 'dup-pkg').length === 1); +} +{ + // Two distinct unresolved entries for the same name@version dedupe to one. + const merged = new Map(); + const unresolved = [ + { name: 'twice', version: '1.0.0', reason: 'cargo-api-failed' }, + { name: 'Twice', version: '1.0.0', reason: 'cargo-no-repo-url' }, + ]; + const out = computeUnaccounted(merged, unresolved); + check('case-insensitive dedupe to one row', out.filter(u => u.name.toLowerCase() === 'twice').length === 1); +} + +// -- 5. sort order ------------------------------------------------------------ +console.log('computeUnaccounted -- sorted by name then version:'); +{ + const merged = new Map(); + const unresolved = [ + { name: 'zeta', version: '1.0.0', reason: 'r' }, + { name: 'alpha', version: '2.0.0', reason: 'r' }, + { name: 'alpha', version: '1.0.0', reason: 'r' }, + ]; + const out = computeUnaccounted(merged, unresolved); + check('first is alpha@1.0.0', out[0].name === 'alpha' && out[0].version === '1.0.0'); + check('second is alpha@2.0.0', out[1].name === 'alpha' && out[1].version === '2.0.0'); + check('last is zeta', out[2].name === 'zeta'); +} + +// -- summary ------------------------------------------------------------------ +console.log(''); +console.log(`merge-notices computeUnaccounted unit checks: ${passed} passed, ${failed} failed`); +if (failed > 0) { + process.exit(1); +} diff --git a/build/azure-pipelines/oss/merge-notices.ts b/build/azure-pipelines/oss/merge-notices.ts index b4f0d9925d6f2..46c78ffde10ca 100644 --- a/build/azure-pipelines/oss/merge-notices.ts +++ b/build/azure-pipelines/oss/merge-notices.ts @@ -116,6 +116,62 @@ function parseNoticeFile(filePath: string): NoticeEntry[] { return entries; } +/** + * Compute the packages NOT accounted for in the final merged NOTICE. A package + * is "unaccounted" if EITHER: + * (a) the scanner tried to resolve it but created no row (it appears in the + * `unresolved` sibling) AND its name is absent from the final merged map + * (name-only match avoids version-drift false positives, and correctly + * excludes packages rescued downstream via a cglicenses.json override), OR + * (b) it IS a row in the final notice but its license body is empty/whitespace. + * + * Pure and exported for unit testing. Informational only — never affects exit code. + */ +export function computeUnaccounted( + merged: Map, + unresolved: Array<{ name: string; version: string; reason: string }> +): Array<{ name: string; version: string; reason: string }> { + const presentNamesLower = new Set([...merged.values()].map(e => e.name.toLowerCase())); + const out: Array<{ name: string; version: string; reason: string }> = []; + + // (a) Genuinely absent: scanner failed AND the name never landed in the notice. + for (const item of unresolved) { + if (!presentNamesLower.has(item.name.toLowerCase())) { + out.push({ name: item.name, version: item.version, reason: item.reason }); + } + } + + // (b) Present but with an empty license body. + for (const entry of merged.values()) { + if (!entry.licenseText || entry.licenseText.trim() === '') { + out.push({ name: entry.name, version: entry.version, reason: 'no-license-text' }); + } + } + + // Dedupe by `@` (a pkg may appear from both sources). + const seen = new Set(); + const deduped: Array<{ name: string; version: string; reason: string }> = []; + for (const u of out) { + const key = `${u.name.toLowerCase()}@${u.version || ''}`; + if (seen.has(key)) { + continue; + } + seen.add(key); + deduped.push(u); + } + + deduped.sort((a, b) => { + const an = a.name.toLowerCase(); + const bn = b.name.toLowerCase(); + if (an !== bn) { + return an.localeCompare(bn); + } + return (a.version || '').localeCompare(b.version || ''); + }); + + return deduped; +} + async function mainAsync(): Promise { const args = parseArgs(process.argv.slice(2)); const cgPath = args['cg'] || ''; @@ -409,19 +465,92 @@ required to debug changes to any libraries licensed under the GNU Lesser General const sizeMB = (output.length / 1024 / 1024).toFixed(2); + // Reconcile the numbers so the summary is self-explaining. Only three inputs + // ADD a row to the final NOTICE: CG packages, brand-new extension-scanner + // packages, and injected overrides. Everything else either lands on a package + // CG already covers (folded -> no new row) or edits an existing row in place + // (count-neutral). "Found" is the gross discovery count across all sources; + // "included" is what actually ships after folding the overlaps. + const extScannerFound = extAdded + extStubOverridden + extSkipped; + const totalFound = cgCount + extScannerFound + overridesInjected; + const totalFolded = extSkipped + extStubOverridden; + const totalIncluded = sorted.length; + + // Load the scanner's unresolved index (packages it tried to resolve but + // produced no row for). Cross-checked against the final merged notice below so + // packages rescued downstream (e.g. a cglicenses.json override) are excluded. + // Never throws — a missing/garbled sibling just yields an empty list. + const unresolvedPath = args['unresolved'] || (extPath ? extPath + '.unresolved.json' : ''); + let unresolvedList: Array<{ name: string; version: string; reason: string }> = []; + if (unresolvedPath && fs.existsSync(unresolvedPath)) { + try { + const raw: unknown = JSON.parse(fs.readFileSync(unresolvedPath, 'utf8')); + if (Array.isArray(raw)) { + unresolvedList = (raw as Array<{ name?: unknown; version?: unknown; reason?: unknown }>) + .filter(item => item && typeof item.name === 'string') + .map(item => ({ + name: item.name as string, + version: typeof item.version === 'string' ? item.version : '', + reason: typeof item.reason === 'string' ? item.reason : 'unresolved', + })); + } + } catch (err) { + console.warn(` WARN: could not read unresolved index ${unresolvedPath}: ${(err as Error).message}`); + } + } + console.log('=== Merge Summary ==='); - console.log(` Total packages: ${sorted.length}`); - console.log(` From CG: ${cgCount}`); - console.log(` From extension scanner: ${extAdded}`); - console.log(` Cargo stub-overrides (beat CG): ${extStubOverridden}`); - console.log(` Extension duplicates skipped: ${extSkipped}`); + console.log(''); + console.log(' Packages found (gross, across all sources):'); + console.log(` Component Governance: ${cgCount}`); + console.log(` Extension scanner: ${extScannerFound} (${extAdded} new, ${extSkipped} already in CG, ${extStubOverridden} stub-overrides)`); if (cglicensesPath) { - console.log(` cglicenses overrides applied: ${overridesApplied} (${overrideEntryCount} merged entries updated)`); - console.log(` cglicenses overrides injected: ${overridesInjected}`); - console.log(` cglicenses overrides stale (skipped, warn-only): ${staleOverrides.length}`); - console.log(` cglicenses overrides unmatched (no usable text): ${unmatchedOverrides.length}`); + console.log(` cglicenses.json injected: ${overridesInjected} (present-but-unlicensed, added by override)`); + } + console.log(` Total packages found: ${totalFound}`); + console.log(''); + console.log(' Folded during merge (scanner hit a package CG already had -- no new row):'); + console.log(` Duplicates skipped (CG text kept): ${extSkipped}`); + console.log(` Stub-overrides (CG text replaced): ${extStubOverridden}`); + console.log(` Total folded: ${totalFolded}`); + console.log(''); + if (cglicensesPath) { + console.log(' Edited in place (count-neutral):'); + console.log(` cglicenses overrides applied: ${overridesApplied} (${overrideEntryCount} entries updated)`); + console.log(` cglicenses overrides stale (skipped): ${staleOverrides.length} (warn-only)`); + console.log(` cglicenses overrides unmatched: ${unmatchedOverrides.length} (no usable text)`); + console.log(''); } + console.log(` Total packages included in shipping ThirdPartyNotices: ${totalIncluded}`); + console.log(` (${totalFound} found - ${totalFolded} folded = ${totalIncluded})`); + + const unaccounted = computeUnaccounted(merged, unresolvedList); + console.log(''); + if (unaccounted.length > 0) { + // Surface as warnings so they show up in the ADO build log's warning count + // and any warning-count tooling. Informational only -- never fails the build. + console.warn(` Packages NOT accounted for in the final NOTICE: ${unaccounted.length}`); + console.warn(' (genuinely absent, or present with an empty license body -- need a cglicenses.json override or upstream fix)'); + for (const u of unaccounted) { + console.warn(` ! ${u.name}@${u.version || '(no version)'} -- ${u.reason}`); + } + } else { + // Zero is good news, not a warning. + console.log(` Packages NOT accounted for in the final NOTICE: ${unaccounted.length}`); + } + + // Defensive: if a future code path mutates the merged map without updating a + // counter, this catches the drift instead of silently shipping a wrong total. + if (totalFound - totalFolded !== totalIncluded) { + console.warn(` WARN: merge accounting mismatch -- found(${totalFound}) - folded(${totalFolded}) = ${totalFound - totalFolded}, but the final NOTICE has ${totalIncluded} packages. A source or override path is uncounted.`); + } + + console.log(''); console.log(` Output: ${outputPath} (${sizeMB} MB)`); } -mainAsync().catch(err => { console.error(err); process.exit(1); }); +// Only run mainAsync() when merge-notices is the entry point -- not when a test +// or another module imports the exported helpers (mirrors scan-licenses.ts). +if (/merge-notices(\.[jt]s)?$/.test(process.argv[1] || '')) { + mainAsync().catch(err => { console.error(err); process.exit(1); }); +} diff --git a/build/azure-pipelines/oss/scan-licenses.ts b/build/azure-pipelines/oss/scan-licenses.ts index 0f8806b59e2b6..9997a8ce31e55 100644 --- a/build/azure-pipelines/oss/scan-licenses.ts +++ b/build/azure-pipelines/oss/scan-licenses.ts @@ -1101,6 +1101,12 @@ async function main(): Promise { // *.stuboverride.json file that merge-notices.ts consumes to let the scanner // entry beat CG on collision (mirrors the presence.json sibling pattern). const stubOverrideKeys = new Set(); + // Unresolved index: packages the scanner TRIED to resolve but for which it + // created NO row at all (fetch failed / no license / API failed). Written as + // a sibling *.unresolved.json that merge-notices.ts cross-checks against the + // final merged NOTICE so packages rescued downstream (e.g. via a + // cglicenses.json override) are excluded from the "not accounted for" list. + const unresolved: Array<{ name: string; version: string; reason: string }> = []; let scanned = 0; let noLicense = 0; @@ -1358,6 +1364,7 @@ async function main(): Promise { continue; } cgManifestFetchFailed++; + unresolved.push({ name, version: reg.version || commitHash.substring(0, 7), reason: 'cgmanifest-fetch-failed' }); console.warn(` FETCH FAILED: ${name} (${repoUrl}@${commitHash.substring(0, 7)}) -- no LICENSE resolved`); continue; } @@ -1484,6 +1491,7 @@ async function main(): Promise { const info = await fetchCratesIoJson(p.name); if (!info) { cargoApiFailed++; + unresolved.push({ name: p.name, version: p.version, reason: 'cargo-api-failed' }); console.warn(` CRATES.IO FAILED: ${p.name}@${p.version} -- no crate info`); return; } @@ -1495,6 +1503,7 @@ async function main(): Promise { // Spec sec. 6.6: a failed crates.io call must log and continue, never crash. if (!Array.isArray(info.versions) || !info.crate || typeof info.crate.id !== 'string') { cargoApiFailed++; + unresolved.push({ name: p.name, version: p.version, reason: 'cargo-api-failed' }); console.warn(` CRATES.IO FAILED: ${p.name}@${p.version} -- unexpected response shape (no versions/crate)`); return; } @@ -1506,6 +1515,7 @@ async function main(): Promise { const repoUrl = getCrateRepository(info); if (!repoUrl) { cargoFetchFailed++; + unresolved.push({ name: p.name, version: p.version, reason: 'cargo-no-repo-url' }); console.warn(` FETCH FAILED: ${p.name}@${p.version} -- no repository URL`); return; } @@ -1518,6 +1528,7 @@ async function main(): Promise { const result = await fetchCargoLicense(repoUrl, p.name, p.version, license); if (!result) { cargoFetchFailed++; + unresolved.push({ name: p.name, version: p.version, reason: 'cargo-no-license-resolved' }); console.warn(` FETCH FAILED: ${p.name}@${p.version} (${repoUrl}) -- no LICENSE resolved at any ref (needs cglicenses.json override)`); return; } @@ -1553,6 +1564,7 @@ async function main(): Promise { // Defense-in-depth: any unexpected throw in this task must log and // continue, never reject (which would crash the build per sec. 6.6). cargoApiFailed++; + unresolved.push({ name: p.name, version: p.version, reason: 'cargo-api-failed' }); console.warn(` CRATES.IO FAILED: ${p.name}@${p.version} -- unexpected error: ${(err as Error).message}`); } }); @@ -2131,6 +2143,14 @@ async function main(): Promise { const stubOverrideList = [...stubOverrideKeys].sort(); fs.writeFileSync(stubOverridePath, JSON.stringify(stubOverrideList, null, '\t'), 'utf8'); + // Write the unresolved index as a sibling file. These are packages the scanner + // tried to resolve but produced NO row for. merge-notices.ts cross-checks this + // against the final merged NOTICE so packages rescued downstream (e.g. a + // cglicenses.json override) are excluded. Mirrors the presence.json sibling. + const unresolvedPath = args['unresolved'] || (outputPath + '.unresolved.json'); + const unresolvedSorted = unresolved.slice().sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); + fs.writeFileSync(unresolvedPath, JSON.stringify(unresolvedSorted, null, '\t'), 'utf8'); + // Summary console.log(''); console.log('=== License Scan Summary ==='); @@ -2188,6 +2208,7 @@ async function main(): Promise { console.log(` Output: ${outputPath}`); console.log(` Presence index (present but unlicensed): ${presence.length}`); console.log(` Presence output: ${presencePath}`); + console.log(` Unresolved (tried, no row created): ${unresolved.length}`); console.log(` Stub-override index: ${stubOverrideList.length}`); console.log(` Stub-override output: ${stubOverridePath}`); diff --git a/build/azure-pipelines/product-build-template.yml b/build/azure-pipelines/product-build-template.yml index 8b66a8f95630c..7ff584ae04135 100644 --- a/build/azure-pipelines/product-build-template.yml +++ b/build/azure-pipelines/product-build-template.yml @@ -82,6 +82,10 @@ parameters: displayName: "Skip tests" type: boolean default: false + - name: VSCODE_USE_LEGACY_OSS_NOTICE + displayName: "Use legacy OSS Notice" + type: boolean + default: false - name: VSCODE_RUN_APISCAN type: boolean default: true diff --git a/build/azure-pipelines/product-build-variables.yml b/build/azure-pipelines/product-build-variables.yml index bcc546b2d1940..d7bef76175b4b 100644 --- a/build/azure-pipelines/product-build-variables.yml +++ b/build/azure-pipelines/product-build-variables.yml @@ -53,6 +53,9 @@ parameters: - name: VSCODE_STEP_ON_IT type: boolean default: false + - name: VSCODE_USE_LEGACY_OSS_NOTICE + type: boolean + default: false variables: - name: VSCODE_PRIVATE_BUILD @@ -85,6 +88,16 @@ variables: value: $[counter(variables['VSCODE_PUBLISH_COUNTER_PREFIX'], 1)] - name: VSCODE_STEP_ON_IT value: ${{ eq(parameters.VSCODE_STEP_ON_IT, true) }} + # Derived as an explicit lowercase string literal so downloadNotice.ts's + # case-sensitive JS check (process.env['VSCODE_OVERWRITE_TPN'] === 'false') + # works. ${{ eq(...) }} would emit 'True'/'False' (capitalized) and silently + # fail that comparison. Checkbox on => keep legacy notice ('false' = no + # overwrite); off => apply CG notice ('true'). + - name: VSCODE_OVERWRITE_TPN + ${{ if eq(parameters.VSCODE_USE_LEGACY_OSS_NOTICE, true) }}: + value: 'false' + ${{ else }}: + value: 'true' - name: VSCODE_BUILD_MACOS_UNIVERSAL value: ${{ and(eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true), eq(parameters.VSCODE_BUILD_MACOS_UNIVERSAL, true)) }} - name: VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index db99e5013997d..7212e74e881b6 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -111,6 +111,10 @@ parameters: displayName: "Skip tests" type: boolean default: false + - name: VSCODE_USE_LEGACY_OSS_NOTICE + displayName: "Use legacy OSS Notice" + type: boolean + default: false variables: - name: VSCODE_PRIVATE_BUILD @@ -143,6 +147,16 @@ variables: value: $[counter(variables['VSCODE_PUBLISH_COUNTER_PREFIX'], 1)] - name: VSCODE_STEP_ON_IT value: ${{ eq(parameters.VSCODE_STEP_ON_IT, true) }} + # Derived as an explicit lowercase string literal so downloadNotice.ts's + # case-sensitive JS check (process.env['VSCODE_OVERWRITE_TPN'] === 'false') + # works. ${{ eq(...) }} would emit 'True'/'False' (capitalized) and silently + # fail that comparison. Checkbox on => keep legacy notice ('false' = no + # overwrite); off => apply CG notice ('true'). + - name: VSCODE_OVERWRITE_TPN + ${{ if eq(parameters.VSCODE_USE_LEGACY_OSS_NOTICE, true) }}: + value: 'false' + ${{ else }}: + value: 'true' - name: VSCODE_BUILD_MACOS_UNIVERSAL value: ${{ and(eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true), eq(parameters.VSCODE_BUILD_MACOS_UNIVERSAL, true)) }} - name: VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME @@ -180,11 +194,49 @@ resources: type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release + - repository: distro + type: github + name: microsoft/vscode-distro + endpoint: Monaco + ref: main + - repository: capi + type: github + name: microsoft/vscode-capi + endpoint: Monaco + ref: main + - repository: encrypt + type: github + name: microsoft/vscode-encrypt + endpoint: Monaco + ref: main + - repository: vsda + type: github + name: microsoft/vsda + endpoint: Monaco + ref: main + - repository: regexp + type: github + name: microsoft/vscode-regexp-languagedetection + endpoint: Monaco + ref: main + - repository: vscode_loc + type: github + name: microsoft/vscode-extensions-loc + endpoint: Monaco + ref: main extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines parameters: sdl: + sourceRepositoriesToScan: + exclude: + - repository: distro + - repository: capi + - repository: encrypt + - repository: vsda + - repository: regexp + - repository: vscode_loc tsa: enabled: true configFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/tsaoptions.json diff --git a/build/azure-pipelines/product-copilot-recovery.yml b/build/azure-pipelines/product-copilot-recovery.yml index a0c52e6830247..6f7888e934c21 100644 --- a/build/azure-pipelines/product-copilot-recovery.yml +++ b/build/azure-pipelines/product-copilot-recovery.yml @@ -10,6 +10,16 @@ resources: name: microsoft/vscode-engineering ref: main endpoint: Monaco + - repository: capi + type: github + name: microsoft/vscode-capi + ref: main + endpoint: Monaco + - repository: vscode_loc + type: github + name: microsoft/vscode-extensions-loc + ref: main + endpoint: Monaco parameters: - name: customNPMRegistry @@ -24,6 +34,12 @@ parameters: variables: - name: VSCODE_QUALITY value: stable + # The 1ES extension template configures the NPM registry itself (via the + # customNPMRegistry / Terrapin parameter below). Set NPM_REGISTRY to 'none' + # so the shared copilot/setup-steps.yml skips its own registry rewrite, which + # would otherwise corrupt package-lock.json resolved URLs when unset. + - name: NPM_REGISTRY + value: none extends: template: azure-pipelines/extension/stable.yml@templates @@ -31,6 +47,24 @@ extends: workingDirectory: ./extensions/copilot l10nSourcePaths: ./extensions/copilot/src nodeVersion: 22.21.x + # The copilot extension is bundled with esbuild and vendors prebuilt native + # modules (e.g. @os-theme, @picovoice/pvrecorder-node) that depend on a + # newer GLIBC than the template's sysroot check allows. The main product + # build (product-copilot.yml) ships these natives without a sysroot or GLIBC + # check, so disable the sysroot here for parity; this also skips the GLIBC/ + # GLIBCXX verification that would otherwise fail on those vendored natives. + buildPlatforms: + - name: Linux + vsceTarget: "" + useSysroot: false + codesignPaths: [] + # Copilot handles localization itself via copilot/l10n-steps.yml (importing + # translated strings from the microsoft/vscode-extensions-loc checkout), + # mirroring product-copilot.yml. Disable the template's own l10n jobs and + # the import-localized-files step: the latter runs before buildSteps and + # expects an implicit self checkout, which is unavailable here because + # build-steps.yml checks out microsoft/vscode-capi. + l10nShouldProcess: false cgIgnoreDirectories: $(Build.SourcesDirectory)/extensions/copilot/script @@ -46,21 +80,17 @@ extends: exit 1 displayName: Validate release branch + - checkout: self + path: s + lfs: true + fetchDepth: 1 + fetchTags: false + retryCountOnTaskFailure: 3 + displayName: Checkout microsoft/vscode + - template: copilot/setup-steps.yml - template: copilot/build-steps.yml - - script: | - set -e - - # The GLIBC check (from the shared extension template) scans every .node - # under $PWD. @github/copilot vendors @picovoice/pvrecorder-node, whose - # linux/x86_64 prebuild depends on GLIBC > 2.28. The native is not - # shipped in the VSIX, so just remove the pvrecorder prebuilds before - # the check runs. - find "$(Build.SourcesDirectory)" \ - -type d -name pvrecorder-node \ - -path "*@picovoice/pvrecorder-node" \ - -print -exec rm -rf {} + - displayName: Remove pvrecorder native binaries before GLIBC check + - template: copilot/l10n-steps.yml uploadSourceMaps: enabled: true @@ -78,6 +108,18 @@ extends: serviceTreeID: '1788a767-5861-45fb-973b-c686b67c5541' enabled: true + # The Copilot build steps check out microsoft/vscode-capi and + # microsoft/vscode-extensions-loc (via the Monaco GitHub App instead of a + # PAT). Every checked-out repository must be declared for the 1ES SDL source + # analysis, so re-declare the template defaults plus capi and vscode_loc here. + sourceRepositoriesToScan: + exclude: + - repository: translations + - repository: vscode-copilot-cache + - repository: templates + - repository: capi + - repository: vscode_loc + ${{ if eq(parameters.customNPMRegistry, false) }}: customNPMRegistry: '' diff --git a/build/azure-pipelines/product-copilot.yml b/build/azure-pipelines/product-copilot.yml index cebe647882fac..d47a23a6d24dc 100644 --- a/build/azure-pipelines/product-copilot.yml +++ b/build/azure-pipelines/product-copilot.yml @@ -5,6 +5,9 @@ parameters: - name: VSCODE_RELEASE type: boolean default: false + - name: VSCODE_UPLOAD_SOURCEMAPS + type: boolean + default: true jobs: - job: Copilot @@ -17,6 +20,7 @@ jobs: value: true steps: - checkout: self + path: s lfs: true fetchDepth: 1 fetchTags: false @@ -93,86 +97,88 @@ jobs: parameters: runIntegrationTests: false - - task: AzureCLI@2 - displayName: Upload source maps to CDN - inputs: - azureSubscription: vscode-cdn - scriptType: bash - addSpnToEnvironment: true - scriptLocation: inlineScript - inlineScript: | - set -e - - WORKING_DIR="$(Build.SourcesDirectory)/extensions/copilot" - SOURCE_MAP_DIR="$WORKING_DIR/dist-sourcemaps" - STORAGE_ACCOUNT="vscodeweb" - - if [ ! -d "$SOURCE_MAP_DIR" ]; then - echo "Source maps directory not found: $SOURCE_MAP_DIR" - echo "Skipping upload." - exit 0 - fi - - PUBLISHER=$(node -p "require('$WORKING_DIR/package.json').publisher") - NAME=$(node -p "require('$WORKING_DIR/package.json').name") - VERSION=$(node -p "require('$WORKING_DIR/package.json').version") - EXTENSION_ID=$(echo "${PUBLISHER}.${NAME}" | tr '[:upper:]' '[:lower:]') - - echo "Extension: $EXTENSION_ID" - echo "Version: $VERSION" - - MAP_FILES=$(find "$SOURCE_MAP_DIR" -name "*.map" -type f) - FILE_COUNT=$(echo "$MAP_FILES" | grep -c . || true) - - if [ "$FILE_COUNT" -eq 0 ]; then - echo "No source map files found in $SOURCE_MAP_DIR" - exit 0 - fi - - echo "Found $FILE_COUNT source map files" - - BLOB_URL="https://${STORAGE_ACCOUNT}.blob.core.windows.net" - PREFIX="sourcemaps/${EXTENSION_ID}/${VERSION}" - - echo "Uploading to: $BLOB_URL/\$web/$PREFIX/" - - UPLOADED=0 - for FILE in $MAP_FILES; do - FILENAME=$(basename "$FILE") - BLOB_NAME="$PREFIX/$FILENAME" - + # Source map upload is only needed for builds that ship the extension. + - ${{ if eq(parameters.VSCODE_UPLOAD_SOURCEMAPS, true) }}: + - task: AzureCLI@2 + displayName: Upload source maps to CDN + inputs: + azureSubscription: vscode-cdn + scriptType: bash + addSpnToEnvironment: true + scriptLocation: inlineScript + inlineScript: | + set -e + + WORKING_DIR="$(Build.SourcesDirectory)/extensions/copilot" + SOURCE_MAP_DIR="$WORKING_DIR/dist-sourcemaps" + STORAGE_ACCOUNT="vscodeweb" + + if [ ! -d "$SOURCE_MAP_DIR" ]; then + echo "Source maps directory not found: $SOURCE_MAP_DIR" + echo "Skipping upload." + exit 0 + fi + + PUBLISHER=$(node -p "require('$WORKING_DIR/package.json').publisher") + NAME=$(node -p "require('$WORKING_DIR/package.json').name") + VERSION=$(node -p "require('$WORKING_DIR/package.json').version") + EXTENSION_ID=$(echo "${PUBLISHER}.${NAME}" | tr '[:upper:]' '[:lower:]') + + echo "Extension: $EXTENSION_ID" + echo "Version: $VERSION" + + MAP_FILES=$(find "$SOURCE_MAP_DIR" -name "*.map" -type f) + FILE_COUNT=$(echo "$MAP_FILES" | grep -c . || true) + + if [ "$FILE_COUNT" -eq 0 ]; then + echo "No source map files found in $SOURCE_MAP_DIR" + exit 0 + fi + + echo "Found $FILE_COUNT source map files" + + BLOB_URL="https://${STORAGE_ACCOUNT}.blob.core.windows.net" + PREFIX="sourcemaps/${EXTENSION_ID}/${VERSION}" + + echo "Uploading to: $BLOB_URL/\$web/$PREFIX/" + + UPLOADED=0 + for FILE in $MAP_FILES; do + FILENAME=$(basename "$FILE") + BLOB_NAME="$PREFIX/$FILENAME" + + az storage blob upload \ + --account-name "$STORAGE_ACCOUNT" \ + --container-name '$web' \ + --name "$BLOB_NAME" \ + --file "$FILE" \ + --content-type "application/json" \ + --content-cache-control "max-age=31536000, public" \ + --auth-mode login \ + --overwrite \ + --only-show-errors + + UPLOADED=$((UPLOADED + 1)) + done + + echo "Successfully uploaded $UPLOADED source maps" + + # Upload commit-to-version mapping so the deminify service + # can resolve the patched extension version from a VS Code commit hash. + COMMIT_HASH="$(Build.SourceVersion)" + MAPPING_BLOB="sourcemaps/${EXTENSION_ID}/commits/${COMMIT_HASH}.json" + echo "{\"version\":\"${VERSION}\",\"extensionId\":\"${EXTENSION_ID}\"}" > /tmp/commit-version.json + + echo "Uploading commit mapping: $COMMIT_HASH -> $VERSION" az storage blob upload \ --account-name "$STORAGE_ACCOUNT" \ --container-name '$web' \ - --name "$BLOB_NAME" \ - --file "$FILE" \ + --name "$MAPPING_BLOB" \ + --file "/tmp/commit-version.json" \ --content-type "application/json" \ - --content-cache-control "max-age=31536000, public" \ + --content-cache-control "no-cache, no-store, must-revalidate" \ --auth-mode login \ --overwrite \ --only-show-errors - UPLOADED=$((UPLOADED + 1)) - done - - echo "Successfully uploaded $UPLOADED source maps" - - # Upload commit-to-version mapping so the deminify service - # can resolve the patched extension version from a VS Code commit hash. - COMMIT_HASH="$(Build.SourceVersion)" - MAPPING_BLOB="sourcemaps/${EXTENSION_ID}/commits/${COMMIT_HASH}.json" - echo "{\"version\":\"${VERSION}\",\"extensionId\":\"${EXTENSION_ID}\"}" > /tmp/commit-version.json - - echo "Uploading commit mapping: $COMMIT_HASH -> $VERSION" - az storage blob upload \ - --account-name "$STORAGE_ACCOUNT" \ - --container-name '$web' \ - --name "$MAPPING_BLOB" \ - --file "/tmp/commit-version.json" \ - --content-type "application/json" \ - --content-cache-control "no-cache, no-store, must-revalidate" \ - --auth-mode login \ - --overwrite \ - --only-show-errors - - echo "Commit mapping uploaded: $MAPPING_BLOB" + echo "Commit mapping uploaded: $MAPPING_BLOB" diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index 0a0ada4ec3d41..6403a6303a159 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -39,9 +39,9 @@ jobs: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get ESRP Secrets" @@ -58,7 +58,7 @@ jobs: npm ci workingDirectory: build env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install build dependencies - download: current diff --git a/build/azure-pipelines/product-quality-checks.yml b/build/azure-pipelines/product-quality-checks.yml index 792c70ebb3890..6ff1658deafae 100644 --- a/build/azure-pipelines/product-quality-checks.yml +++ b/build/azure-pipelines/product-quality-checks.yml @@ -20,9 +20,9 @@ jobs: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -68,7 +68,7 @@ jobs: done workingDirectory: build env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -78,7 +78,7 @@ jobs: SYSROOT_ARCH="amd64" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' env: VSCODE_ARCH: x64 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Download vscode sysroots - script: | @@ -100,7 +100,7 @@ jobs: VSCODE_ARCH: x64 ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -122,7 +122,6 @@ jobs: - script: node build/azure-pipelines/common/checkDistroCommit.ts displayName: Check distro commit env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" BUILD_SOURCEBRANCH: "$(Build.SourceBranch)" continueOnError: true condition: and(succeeded(), eq(lower(variables['VSCODE_PUBLISH']), 'true')) @@ -131,7 +130,7 @@ jobs: - script: npm exec -- npm-run-all2 -lp core-ci hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check test-build-scripts env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Compile & Hygiene - script: npm run download-builtin-extensions-cg @@ -149,6 +148,7 @@ jobs: displayName: "Generate ThirdPartyNotices (CG)" inputs: outputfile: $(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt + retryCountOnTaskFailure: 3 continueOnError: true # ========================================================================= @@ -160,8 +160,13 @@ jobs: # from a prior build. # # Fallback order: - # 1. Most recent successful run on the CURRENT branch - # 2. Most recent successful run on `main` + # 1. Most recent run on the CURRENT branch + # 2. Most recent run on `main` + # + # Carry-forward: the cache download reads only the SINGLE latest build, so a + # sustained CG outage (>1 build) breaks the chain unless every build keeps a + # copy. The "Upload CG NOTICE artifact" step below runs on the fallback path + # too, handing the last-good NOTICE forward. Ref: builds 450982->451029. # # TODO: consider enforcing a max-age check (~30 days) on cached NOTICEs. # Today the only signal is the warning emitted below. @@ -171,7 +176,27 @@ jobs: # run fresh against current HEAD, so extension licenses and # cglicenses.json overrides are always current-PR-accurate. # ========================================================================= - - task: DownloadPipelineArtifact@2 + + # Detect up front whether CG produced a usable NOTICE. The cache-download + # tasks below run ONLY when it did not -- otherwise they execute on the + # happy path and, when no prior cache artifact exists (e.g. the first build + # on a branch), fail-with-continueOnError and mark the whole build + # partiallySucceeded for no real reason. Uses the same >1KB heuristic as + # the "apply NOTICE fallback" step below so the two always agree. + - script: | + set -u + GEN="$(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt" + if [ -f "$GEN" ] && [ "$(wc -c < "$GEN")" -gt 1024 ]; then + echo "##[section]CG NOTICE generation succeeded; skipping cache download." + echo "##vso[task.setvariable variable=CG_NOTICE_OK]true" + else + echo "##[warning]CG NOTICE generation failed or produced empty output; will attempt cache fallback." + echo "##vso[task.setvariable variable=CG_NOTICE_OK]false" + fi + displayName: "Cache: detect whether CG NOTICE needs fallback" + continueOnError: true + + - task: DownloadBuildArtifacts@1 displayName: "Cache: download last good NOTICE (same branch)" inputs: buildType: specific @@ -181,11 +206,20 @@ jobs: branchName: $(Build.SourceBranch) allowPartiallySucceededBuilds: true artifactName: notice_output - itemPattern: "**/{ThirdPartyNotices.generated.txt,notice-meta.txt}" - targetPath: $(Pipeline.Workspace)/cached-notice-branch + # NB: one glob per line. DownloadBuildArtifacts@1 runs minimatch with + # brace expansion disabled, so a "**/{a,b}" pattern matches NOTHING and + # silently downloads 0 files (observed in build 450982, the first real + # CG outage post-cutover: the artifact downloaded but every file was + # excluded, so the apply-fallback step saw no cache). Multi-line plain + # globs are matched reliably. + itemPattern: | + **/ThirdPartyNotices.generated.txt + **/notice-meta.txt + downloadPath: $(Pipeline.Workspace)/cached-notice-branch continueOnError: true + condition: and(succeeded(), ne(variables.CG_NOTICE_OK, 'true')) - - task: DownloadPipelineArtifact@2 + - task: DownloadBuildArtifacts@1 displayName: "Cache: download last good NOTICE (main fallback)" inputs: buildType: specific @@ -195,18 +229,22 @@ jobs: branchName: refs/heads/main allowPartiallySucceededBuilds: true artifactName: notice_output - itemPattern: "**/{ThirdPartyNotices.generated.txt,notice-meta.txt}" - targetPath: $(Pipeline.Workspace)/cached-notice-main + # See note on the same-branch download above: brace patterns match 0 + # files in DownloadBuildArtifacts@1. Keep one plain glob per line. + itemPattern: | + **/ThirdPartyNotices.generated.txt + **/notice-meta.txt + downloadPath: $(Pipeline.Workspace)/cached-notice-main continueOnError: true - condition: ne(variables['Build.SourceBranch'], 'refs/heads/main') + condition: and(succeeded(), ne(variables.CG_NOTICE_OK, 'true'), ne(variables['Build.SourceBranch'], 'refs/heads/main')) - script: | set -u GEN="$(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt" - BRANCH_CACHE="$(Pipeline.Workspace)/cached-notice-branch/ThirdPartyNotices.generated.txt" - BRANCH_META="$(Pipeline.Workspace)/cached-notice-branch/notice-meta.txt" - MAIN_CACHE="$(Pipeline.Workspace)/cached-notice-main/ThirdPartyNotices.generated.txt" - MAIN_META="$(Pipeline.Workspace)/cached-notice-main/notice-meta.txt" + BRANCH_CACHE="$(Pipeline.Workspace)/cached-notice-branch/notice_output/ThirdPartyNotices.generated.txt" + BRANCH_META="$(Pipeline.Workspace)/cached-notice-branch/notice_output/notice-meta.txt" + MAIN_CACHE="$(Pipeline.Workspace)/cached-notice-main/notice_output/ThirdPartyNotices.generated.txt" + MAIN_META="$(Pipeline.Workspace)/cached-notice-main/notice_output/notice-meta.txt" # Emit what a cached NOTICE was built from, if we have the sidecar. # The sidecar is written by the upload step on every successful run. @@ -221,7 +259,8 @@ jobs: fi } - # `notice@0` succeeded if the file exists and is non-trivial (>1KB). + # Safety net: never overwrite a valid NOTICE. If CG produced a good + # file, skip the fallback even if we were reached by mistake. if [ -f "$GEN" ] && [ "$(wc -c < "$GEN")" -gt 1024 ]; then echo "##[section]CG NOTICE generation succeeded; no fallback needed." echo "##vso[task.setvariable variable=NOTICE_FROM_CACHE]false" @@ -251,38 +290,58 @@ jobs: echo "##vso[task.setvariable variable=NOTICE_FROM_CACHE]none" displayName: "Cache: apply NOTICE fallback if CG failed" continueOnError: true + condition: and(succeeded(), ne(variables.CG_NOTICE_OK, 'true')) - script: | + set -u + GEN="$(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt" echo "=== CG NOTICE artifact ===" - if [ -f "$(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt" ]; then - SIZE=$(wc -c < "$(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt") - echo "Generated file size: $SIZE bytes (NOTICE_FROM_CACHE=$NOTICE_FROM_CACHE)" - cp "$(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt" "$(Build.ArtifactStagingDirectory)/ThirdPartyNotices.generated.txt" - echo "##vso[artifact.upload containerfolder=notice_output;artifactname=notice_output]$(Build.ArtifactStagingDirectory)/ThirdPartyNotices.generated.txt" - - # Write a sidecar so future runs (and humans) know what this NOTICE - # was built from. Consumed by the "Cache: apply NOTICE fallback" step - # of subsequent builds when they fall back to this artifact. - # Only meaningful when this run generated the NOTICE fresh; if we - # fell back to a cache, skip writing the sidecar so we don't - # mis-attribute someone else's NOTICE to this commit. - if [ "${NOTICE_FROM_CACHE:-false}" = "false" ]; then - { - echo "built_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "source_branch: $(Build.SourceBranch)" - echo "source_commit: $(Build.SourceVersion)" - echo "build_id: $(Build.BuildId)" - echo "build_number: $(Build.BuildNumber)" - echo "size_bytes: $SIZE" - } > "$(Build.ArtifactStagingDirectory)/notice-meta.txt" - echo "##vso[artifact.upload containerfolder=notice_output;artifactname=notice_output]$(Build.ArtifactStagingDirectory)/notice-meta.txt" + + # Publish on both paths: fresh CG success AND carry-forward (re-publishing + # a cached NOTICE so the chain survives a multi-build outage; see above). + if [ ! -f "$GEN" ] || [ "$(wc -c < "$GEN")" -le 1024 ]; then + echo "No usable ThirdPartyNotices.generated.txt to publish; skipping (no CG output and no cached fallback)." + exit 0 + fi + + SIZE=$(wc -c < "$GEN") + echo "Generated file size: $SIZE bytes (source: CG_NOTICE_OK=${CG_NOTICE_OK:-unset}, NOTICE_FROM_CACHE=${NOTICE_FROM_CACHE:-unset})" + cp "$GEN" "$(Build.ArtifactStagingDirectory)/ThirdPartyNotices.generated.txt" + echo "##vso[artifact.upload containerfolder=notice_output;artifactname=notice_output]$(Build.ArtifactStagingDirectory)/ThirdPartyNotices.generated.txt" + + # Sidecar for the next build's fallback. Fresh run records this build's + # provenance; carry-forward preserves the original + appends a marker. + META_OUT="$(Build.ArtifactStagingDirectory)/notice-meta.txt" + if [ "${CG_NOTICE_OK:-}" = "true" ]; then + { + echo "built_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "source_branch: $(Build.SourceBranch)" + echo "source_commit: $(Build.SourceVersion)" + echo "build_id: $(Build.BuildId)" + echo "build_number: $(Build.BuildNumber)" + echo "size_bytes: $SIZE" + } > "$META_OUT" + else + case "${NOTICE_FROM_CACHE:-}" in + branch) SRC_META="$(Pipeline.Workspace)/cached-notice-branch/notice_output/notice-meta.txt" ;; + main) SRC_META="$(Pipeline.Workspace)/cached-notice-main/notice_output/notice-meta.txt" ;; + *) SRC_META="" ;; + esac + if [ -n "$SRC_META" ] && [ -f "$SRC_META" ]; then + cp "$SRC_META" "$META_OUT" else - echo "Skipping notice-meta.txt sidecar: NOTICE_FROM_CACHE=$NOTICE_FROM_CACHE (don't re-publish someone else's metadata as our own)" + echo "original_provenance: unavailable (cached NOTICE predates the notice-meta.txt sidecar)" > "$META_OUT" fi - else - echo "ERROR: ThirdPartyNotices.generated.txt not found (CG failed AND no cache available)" + { + echo "carried_forward_by_build: $(Build.BuildId)" + echo "carried_forward_by_number: $(Build.BuildNumber)" + echo "carried_forward_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "carried_forward_reason: CG NOTICE generation failed; re-published last-good NOTICE to keep the cache chain alive." + } >> "$META_OUT" fi - displayName: "Upload CG NOTICE artifact" + echo "##vso[artifact.upload containerfolder=notice_output;artifactname=notice_output]$META_OUT" + displayName: "Upload CG NOTICE artifact (fresh or carried-forward)" + condition: and(succeeded(), or(eq(variables.CG_NOTICE_OK, 'true'), in(variables.NOTICE_FROM_CACHE, 'branch', 'main'))) continueOnError: true - script: | @@ -292,10 +351,6 @@ jobs: --repo "$(Build.SourcesDirectory)" \ --cg "$(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt" \ --output "$(Build.ArtifactStagingDirectory)/ThirdPartyNotices.extensions.txt" - displayName: "Scan extension-bundled licenses" - continueOnError: true - - - script: | node .oss-build-out/merge-notices.js \ --cg "$(Build.SourcesDirectory)/ThirdPartyNotices.generated.txt" \ --extensions "$(Build.ArtifactStagingDirectory)/ThirdPartyNotices.extensions.txt" \ @@ -306,7 +361,7 @@ jobs: # Upload all artifacts echo "##vso[artifact.upload containerfolder=notice_output;artifactname=notice_output]$(Build.ArtifactStagingDirectory)/ThirdPartyNotices.extensions.txt" echo "##vso[artifact.upload containerfolder=notice_output;artifactname=notice_output]$(Build.ArtifactStagingDirectory)/ThirdPartyNotices.new.txt" - displayName: "Merge CG + extension notices into .new.txt" + displayName: "Build merged NOTICE (scan extensions + merge into .new.txt)" continueOnError: true - task: AzureCLI@2 diff --git a/build/azure-pipelines/product-smoke-flaky.yml b/build/azure-pipelines/product-smoke-flaky.yml new file mode 100644 index 0000000000000..de729e6d13944 --- /dev/null +++ b/build/azure-pipelines/product-smoke-flaky.yml @@ -0,0 +1,209 @@ +# Daily smoke-test flaky-detection pipeline. +# +# Builds VS Code from source on Linux, Windows and macOS and runs ONLY the +# Electron smoke tests, repeated 20 times per platform. Each repetition is its +# own step (continueOnError) so a single flaky failure does not abort the run and +# every iteration produces its own timeline record. The pass/fail tally is read +# back from the build timeline by the vscode-probot `SmokeFlaky` Azure Function +# (wired up via an ADO service hook on build completion), which posts a summary +# to the Slack #build channel. +# +# We build from source rather than testing a published build on purpose: product +# builds are not always released, so depending on a published artifact would make +# us miss the daily schedule. Building here keeps the run self-contained. + +pr: none +trigger: none + +schedules: + # Once per day at 04:00 UTC (= 5am CET / 6am CEST) — a quiet window for the ADO + # org. `always: true` forces the run even when there are no new commits, since + # flaky behavior is environmental and worth sampling every day regardless of + # source changes. + - cron: "0 4 * * *" + displayName: Daily at 04:00 UTC / 5am CET (smoke flaky detection) + branches: + include: + - main + always: true + +parameters: + - name: VSCODE_QUALITY + displayName: Quality + type: string + default: insider + values: + - exploration + - insider + - stable + - name: NPM_REGISTRY + displayName: "Custom NPM Registry" + type: string + default: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry/" + # The smoke test is run once per entry in this list. 20 entries => 20 runs per + # platform (the flaky-detection sample size). Exposed as an object so it can be + # shortened for manual debugging runs from the ADO UI. + - name: iterations + displayName: "Smoke test iterations (one run per entry)" + type: object + default: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + - name: VSCODE_BUILD_MACOS + displayName: "🎯 macOS arm64" + type: boolean + default: true + - name: VSCODE_BUILD_LINUX + displayName: "🎯 Linux x64" + type: boolean + default: true + - name: VSCODE_BUILD_WIN32 + displayName: "🎯 Windows x64" + type: boolean + default: true + +variables: + - name: VSCODE_QUALITY + value: ${{ parameters.VSCODE_QUALITY }} + - name: NPM_REGISTRY + value: ${{ parameters.NPM_REGISTRY }} + # Compile templates skip publish/codesign/artifact-staging when VSCODE_CIBUILD + # is true; it's passed directly as a job parameter. VSCODE_STEP_ON_IT must be + # 'false' to keep the build steps enabled. + - name: VSCODE_STEP_ON_IT + value: false + - name: VSCODE_MIXIN_REPO + value: microsoft/vscode-distro + - name: VSCODE_OVERWRITE_TPN + value: "true" + # The Copilot extension build patches its package.json version for pre-release + # (non-stable) quality and requires a per-build counter (0-99, reset daily) to + # keep versions unique. Mirrors product-build.yml. The counter resets each day + # per quality+branch, so this daily pipeline stays at a low value. + - name: VSCODE_PUBLISH_COUNTER_PREFIX + value: $[format('{0:yyyyMMdd}-{1}-{2}', pipeline.startTime, variables['VSCODE_QUALITY'], variables['Build.SourceBranch'])] + - name: VSCODE_PUBLISH_COUNTER + value: $[counter(variables['VSCODE_PUBLISH_COUNTER_PREFIX'], 1)] + - name: skipComponentGovernanceDetection + value: true + - name: Codeql.SkipTaskAutoInjection + value: true + +name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.VSCODE_QUALITY }} smoke flaky)" + +resources: + repositories: + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + - repository: distro + type: github + name: microsoft/vscode-distro + endpoint: Monaco + ref: main + - repository: capi + type: github + name: microsoft/vscode-capi + endpoint: Monaco + ref: main + - repository: encrypt + type: github + name: microsoft/vscode-encrypt + endpoint: Monaco + ref: main + - repository: vsda + type: github + name: microsoft/vsda + endpoint: Monaco + ref: main + - repository: regexp + type: github + name: microsoft/vscode-regexp-languagedetection + endpoint: Monaco + ref: main + - repository: vscode_loc + type: github + name: microsoft/vscode-extensions-loc + endpoint: Monaco + ref: main + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + sdl: + sourceRepositoriesToScan: + exclude: + - repository: distro + - repository: capi + - repository: encrypt + - repository: vsda + - repository: regexp + - repository: vscode_loc + tsa: + enabled: false + codeql: + compiled: + enabled: false + justificationForDisabling: Smoke test repetition only, not a shipping build + credscan: + suppressionsFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/CredScanSuppressions.json + eslint: + enabled: false + binskim: + enabled: false + sourceAnalysisPool: 1es-windows-2022-x64 + createAdoIssuesForJustificationsForDisablement: false + stages: + # The per-OS compile template unconditionally waits (up to 30 min) for a + # `Copilot` job to publish the `copilot_vsix` artifact, which is mixed into + # the build. This stage produces it — mirroring product-build.yml. It runs + # in parallel with the build stages (dependsOn: []); the download step polls + # the whole-run timeline for the artifact. Without it every build stage + # fails at "Download Copilot VSIX". + - stage: Copilot + dependsOn: [] + pool: + name: 1es-ubuntu-22.04-x64 + os: linux + jobs: + - template: build/azure-pipelines/product-copilot.yml@self + parameters: + VSCODE_PUBLISH: false + VSCODE_RELEASE: false + # This is a test-only run; don't push the Copilot extension's source + # maps to the CDN. + VSCODE_UPLOAD_SOURCEMAPS: false + + - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - stage: Linux + dependsOn: [] + pool: + name: 1es-ubuntu-22.04-x64 + os: linux + jobs: + - template: build/azure-pipelines/linux/product-smoke-flaky-linux.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + iterations: ${{ parameters.iterations }} + + - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: + - stage: Windows + dependsOn: [] + pool: + name: 1es-windows-2022-x64 + os: windows + jobs: + - template: build/azure-pipelines/win32/product-smoke-flaky-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + iterations: ${{ parameters.iterations }} + + - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: + - stage: macOS + dependsOn: [] + pool: + name: AcesShared + os: macOS + jobs: + - template: build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml@self + parameters: + iterations: ${{ parameters.iterations }} diff --git a/build/azure-pipelines/web/product-build-web-node-modules.yml b/build/azure-pipelines/web/product-build-web-node-modules.yml index 75a0cc6cd6e75..fdd15d54f1a10 100644 --- a/build/azure-pipelines/web/product-build-web-node-modules.yml +++ b/build/azure-pipelines/web/product-build-web-node-modules.yml @@ -18,9 +18,9 @@ jobs: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -73,7 +73,7 @@ jobs: env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 86d6215f31cf0..7666105b4d147 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -30,9 +30,9 @@ jobs: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -89,7 +89,7 @@ jobs: env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -119,7 +119,7 @@ jobs: - script: npm run gulp core-ci env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Compile - script: npx deemon --attach -- node build/azure-pipelines/common/downloadCopilotVsix.ts @@ -135,7 +135,7 @@ jobs: tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-web echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Build - task: AzureCLI@2 diff --git a/build/azure-pipelines/win32/product-build-win32-cli.yml b/build/azure-pipelines/win32/product-build-win32-cli.yml index c01e421e6d443..fd56ba730a085 100644 --- a/build/azure-pipelines/win32/product-build-win32-cli.yml +++ b/build/azure-pipelines/win32/product-build-win32-cli.yml @@ -35,6 +35,13 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub token" + inputs: + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" + - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 diff --git a/build/azure-pipelines/win32/product-build-win32-node-modules.yml b/build/azure-pipelines/win32/product-build-win32-node-modules.yml index 6780073f57af7..9167e5b6a004b 100644 --- a/build/azure-pipelines/win32/product-build-win32-node-modules.yml +++ b/build/azure-pipelines/win32/product-build-win32-node-modules.yml @@ -29,9 +29,9 @@ jobs: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - powershell: node build/setup-npm-registry.ts $env:NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -76,7 +76,7 @@ jobs: npm_config_foreground_scripts: "true" ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" retryCountOnTaskFailure: 5 displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/win32/product-smoke-flaky-win32.yml b/build/azure-pipelines/win32/product-smoke-flaky-win32.yml new file mode 100644 index 0000000000000..e1241ae3c70b3 --- /dev/null +++ b/build/azure-pipelines/win32/product-smoke-flaky-win32.yml @@ -0,0 +1,66 @@ +parameters: + - name: VSCODE_QUALITY + type: string + - name: iterations + type: object + +jobs: + - job: WindowsSmokeFlaky + displayName: Windows (Electron) + timeoutInMinutes: 300 + variables: + VSCODE_ARCH: x64 + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/crashes + artifactName: crash-dump-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Crash Reports + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/logs + artifactName: logs-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Log Files + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + steps: + # Build VS Code from source. No VSCODE_RUN_*_TESTS flags are passed, so the + # compile template builds the client/server but skips compiling the test + # suites and skips the unit/integration/smoke test template entirely — we + # run only the smoke tests below, nothing else. + - template: ./steps/product-build-win32-compile.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_CIBUILD: true + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + + - powershell: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" + env: + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + displayName: Download Electron and Playwright + retryCountOnTaskFailure: 3 + + # The compile template only compiles the smoke suite when a test flag is + # set; since we pass none, compile it explicitly here. + - powershell: npm run compile --prefix test/smoke + displayName: Compile smoke tests + + # Guarantee the artifact directories exist so the 1ES output publishing + # never fails on a missing path. The smoke runner writes logs under + # .build/logs and crash dumps under .build/crashes (only on a crash). + - powershell: New-Item -ItemType Directory -Force -Path .build\crashes, .build\logs | Out-Null + displayName: Ensure artifact directories exist + + # Run the Electron smoke test once per iteration. continueOnError lets every + # iteration run even if some fail, and gives each run its own timeline record + # so the SmokeFlaky function can tally passes vs. failures. + - ${{ each i in parameters.iterations }}: + - powershell: npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + displayName: "🧪 Smoke test iteration ${{ i }}/${{ length(parameters.iterations) }} (Electron)" + continueOnError: true + timeoutInMinutes: 20 diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index 1d41892bf8d31..65c3423d64800 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -22,9 +22,9 @@ steps: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - powershell: node build/setup-npm-registry.ts $env:NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -88,7 +88,7 @@ steps: npm_config_foreground_scripts: "true" ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" retryCountOnTaskFailure: 5 displayName: Install dependencies @@ -106,12 +106,13 @@ steps: - powershell: npm run gulp core-ci env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Compile - powershell: npm run gulp "vscode-symbols-win32-${{ parameters.VSCODE_ARCH }}" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Download Symbols - powershell: | diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml index 2539ad4f9180c..416c522d7d195 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml @@ -33,9 +33,9 @@ steps: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + azureSubscription: vscode-oss-build-secrets + KeyVaultName: vscode-oss-build-secrets + SecretsFilter: "github-token-code-oss" - powershell: node build/setup-npm-registry.ts $env:NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) @@ -84,7 +84,7 @@ steps: npm_config_foreground_scripts: "true" ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" retryCountOnTaskFailure: 5 displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -111,6 +111,16 @@ steps: - powershell: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality + # Start pulling the CG-generated NOTICE from the parallel Quality stage + # in the background (overwrites repo-root ThirdPartyNotices.txt before packaging). + # Publish builds only (CIBUILD=false) — CI test jobs never ship a notice. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - pwsh: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Pull CG NOTICE (background) + - template: ../../common/install-builtin-extensions.yml@self - template: ../../common/agent-sdk-produce.yml@self @@ -126,7 +136,7 @@ steps: - powershell: npm run gulp core-ci env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" displayName: Compile - script: node build/azure-pipelines/common/extract-telemetry.ts @@ -157,6 +167,17 @@ steps: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Download Copilot VSIX + # Block on the CG NOTICE download and overwrite ThirdPartyNotices.txt + # right before gulp packages it. Non-fatal: falls back to legacy on any problem. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - pwsh: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Apply CG NOTICE + # deemon --attach can exit non-zero if the background daemon died; never fail for that. + continueOnError: true + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -168,7 +189,8 @@ steps: echo "##vso[task.setvariable variable=BUILT_CLIENT]true" echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build client # Note: the appx prepare step has to follow Build client step since build step replaces the template @@ -199,7 +221,8 @@ steps: echo "##vso[task.setvariable variable=BUILT_SERVER]true" echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build server - powershell: | @@ -213,7 +236,8 @@ steps: echo "##vso[task.setvariable variable=BUILT_WEB]true" echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)-web" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Build server (web) - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: diff --git a/build/azure-pipelines/win32/steps/product-build-win32-test.yml b/build/azure-pipelines/win32/steps/product-build-win32-test.yml index 187856b227229..128ff569ca03e 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-test.yml @@ -11,7 +11,8 @@ parameters: steps: - powershell: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) displayName: Download Electron and Playwright retryCountOnTaskFailure: 3 diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 7db96c8acfc3b..37aeada5f98f6 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -6c25b7496a0f3f4e325965ac3d934f9460e6b39fc2aa05b2aefecb1e212e7535 *chromedriver-v42.2.0-darwin-arm64.zip -7ea0a5378a615b816c6652c1e8f62b25fee751f34c669eb3eff122dcde98dffc *chromedriver-v42.2.0-darwin-x64.zip -8f6c2462e05491ab7a6ea6492fe7f62c8de1b01900978dd3dcbc878fde6f5e3e *chromedriver-v42.2.0-linux-arm64.zip -72cdb458b48306ec4939021198696926651a6a6dd47b8acf4486d77689ffe88f *chromedriver-v42.2.0-linux-armv7l.zip -ebe0fb1e5eb8a83a20612d660191d8292826d46d93701fe6390a9a5ab69aede0 *chromedriver-v42.2.0-linux-x64.zip -f19d5491a6c5d2970b05a42e1e2a5bcab75206423cea9dfe8ef5c2b4a0ceb38b *chromedriver-v42.2.0-mas-arm64.zip -30c88509d9640b4bf0066da7b6df7c4bb61d894df20941daaeeead598da40c28 *chromedriver-v42.2.0-mas-x64.zip -a6a21d19b84938d32ea75ad19013370c8937536c049d4d5c44175fbcf3703c5b *chromedriver-v42.2.0-win32-arm64.zip -da076702e72317843754e72cc1895c4d223e423f177061342ae5aec78a7281a9 *chromedriver-v42.2.0-win32-ia32.zip -97074022e6b5048b0bd9932c011e8377148c04b0e5455ea2e24bfc048d888297 *chromedriver-v42.2.0-win32-x64.zip -4012c6a738d83799544cabd04e41c8eba2467e4de573664326eaea174f2f8b47 *electron-api.json -bffe18be718c641a0fdfa8bf2ab82dbce462b8809be97898a9c4dfd136be071d *electron-v42.2.0-darwin-arm64-dsym-snapshot.zip -5a341581905b84ae62b4f7664c0fd950ec78fef172042dd8b55dc2a8d9aea66a *electron-v42.2.0-darwin-arm64-dsym.tar.xz -110b25eadfd680ad44ba5b43bf59dcf31297976ee128735e3ef9bbc2a35758cf *electron-v42.2.0-darwin-arm64-symbols.zip -f45f80da0a2d005530b70f6f6b00756dbf875947a21e533041a05b3c4d629f79 *electron-v42.2.0-darwin-arm64.zip -aefc2008c08cd3795cf997a5c5d88772497f460c5cc6f5008b01c412d7534417 *electron-v42.2.0-darwin-x64-dsym-snapshot.zip -a6b70b51de1f05541e42138d02768ef72940c125379b0d7a929fc3e5f2e1eb75 *electron-v42.2.0-darwin-x64-dsym.tar.xz -2d64a5bf8af6b9dccecde9ac09f20cbae36a76be537c4b3e5c3a238add5a4a01 *electron-v42.2.0-darwin-x64-symbols.zip -cd6a2d4feca84b7e7b2c4a5a13a443fc3c1173e77b05f798deaab2f0f41002a1 *electron-v42.2.0-darwin-x64.zip -2c0e81bc0e82c9e1966d4042b565dbf4924bd6365c08e6cc715001b3842ff9cd *electron-v42.2.0-linux-arm64-debug.zip -30983431149fc5a813c2145843744292e2553577f19d75aa09cbf8f7100d9319 *electron-v42.2.0-linux-arm64-symbols.zip -1f2037dbdcb8b1327b855ec15fbe3fb8a7f27786b331d17866e88377a0606ad8 *electron-v42.2.0-linux-arm64.zip -948553edfe3fa5272327d867b68aceb6e272cdeb68df4a40a6ffa045976531d2 *electron-v42.2.0-linux-armv7l-debug.zip -fed24d4f0c4fa40c3dfe38fdea423d6f36dc09630a614fb9060373fe30f310c4 *electron-v42.2.0-linux-armv7l-symbols.zip -00de1cc51859a4e064a2178cb29c899feadfabf24d926d8f684c30e6ab9b72a7 *electron-v42.2.0-linux-armv7l.zip -e0a0c2f97e97321ca194bf2ed7275ad6b31c0c0f4f7e4989838c4fd74728f306 *electron-v42.2.0-linux-x64-debug.zip -cf65567a4c5ef25c24cbdc8a6059a419773756f79313a1e682272c42f5716f2a *electron-v42.2.0-linux-x64-symbols.zip -9caeeb15dada37cb3a2d80bf0f5899d175db026a4def11560890bd2f19684909 *electron-v42.2.0-linux-x64.zip -69ad3951065eb33bb6533f872267828e4ea728ca7d539c649cebc966c0ea1bab *electron-v42.2.0-mas-arm64-dsym-snapshot.zip -38c4beeb71171ab97f55e9a213aa44a45e20e62cc73207532f29ff70a2698539 *electron-v42.2.0-mas-arm64-dsym.tar.xz -359be04c4da1b1979f0e1275b3abd5bd2cbab43c2290d71bfdeb174c1f42beba *electron-v42.2.0-mas-arm64-symbols.zip -535aca88dbe2977d22dc63e47887d09c7e41e8ccf4c0f23d67b7ce593e2341dd *electron-v42.2.0-mas-arm64.zip -e621151d901eeab917e225c97e102fa33ba5225c5cd5b8c152948d8e4a60d104 *electron-v42.2.0-mas-x64-dsym-snapshot.zip -0d33ddacaa69b923fe0117f6ed7d92c8918de6e98daa5f0e9462acf1ea758c5e *electron-v42.2.0-mas-x64-dsym.tar.xz -1527430d26a58505c0e3e52f27afc5aa5feffa5dd17d83d1b89ebcb16dfb4c0c *electron-v42.2.0-mas-x64-symbols.zip -576abe887a65587910ffc481fcaac39bb42e2791c54ae3a2c000431a44299cd2 *electron-v42.2.0-mas-x64.zip -d4e06302625806acf6a39b15a22835de6d4c38ce0cedfe1716d4c0df8416fc2b *electron-v42.2.0-win32-arm64-pdb.zip -09792a89e357258afae9d3abe51f42592062bafb1b98e80d77daa1cc554e26f2 *electron-v42.2.0-win32-arm64-symbols.zip -615b0145f304e0eea0ffd20c0fcf0e8e7e5255e1c3f3ae0577725a16ebb91994 *electron-v42.2.0-win32-arm64-toolchain-profile.zip -1e6b5639cbbb0134f41e5cb62a283ebc34bc580a8fb21349d711f275d60f9705 *electron-v42.2.0-win32-arm64.zip -5b5803e32a2ddfb51bbf4a0097a6fb48f964b2844150cec9c6fb48d3eef16865 *electron-v42.2.0-win32-ia32-pdb.zip -218ce2ebacc35de9bc9ac83a1b65551fd53c9622b71516a484bcfed0aec2c4a6 *electron-v42.2.0-win32-ia32-symbols.zip -615b0145f304e0eea0ffd20c0fcf0e8e7e5255e1c3f3ae0577725a16ebb91994 *electron-v42.2.0-win32-ia32-toolchain-profile.zip -1a866e0634ff95f83a043c7c6738e148f38362ed0afa60aa5eb8dcab16def7b5 *electron-v42.2.0-win32-ia32.zip -82329a1c558392b1d2a80cfc24d1463cf88fd0bb7b542eee24305ae099935bf6 *electron-v42.2.0-win32-x64-pdb.zip -00b8c0437bb75b0db1dfa2bf23e6a6d2436c0665e2a627bd2732ccab0fd1e648 *electron-v42.2.0-win32-x64-symbols.zip -615b0145f304e0eea0ffd20c0fcf0e8e7e5255e1c3f3ae0577725a16ebb91994 *electron-v42.2.0-win32-x64-toolchain-profile.zip -6e034b748ad5ed9445bd3da4b7d0792ed49556774a541217b507c156b00dd69a *electron-v42.2.0-win32-x64.zip -acb2fdd6e99a2056ff88fe26d94b141922e30c5a6a2b11868aa5e708ba30266d *electron.d.ts -3099226c4eb0c13134bbdd970c0fac8a372547b401c54552a89fa1689470dbd7 *ffmpeg-v42.2.0-darwin-arm64.zip -1f4fac2e8f4f8b136bc29ce6be50598f217300e10c1a203b6b893f623ba516ba *ffmpeg-v42.2.0-darwin-x64.zip -1fd892c2195d89a7d4f926167c122c34ab7ce15a61b75c8e5ba0cbfc47ebdb4f *ffmpeg-v42.2.0-linux-arm64.zip -6b6a200705ded7211b02d7ee56396d99c2003aa6b0b83bdd8c89f95702ea459d *ffmpeg-v42.2.0-linux-armv7l.zip -ffb78ccf0b2cbf1a1c0da2a69d8021337ae5352642a2d84adae951f95c771696 *ffmpeg-v42.2.0-linux-x64.zip -3099226c4eb0c13134bbdd970c0fac8a372547b401c54552a89fa1689470dbd7 *ffmpeg-v42.2.0-mas-arm64.zip -1f4fac2e8f4f8b136bc29ce6be50598f217300e10c1a203b6b893f623ba516ba *ffmpeg-v42.2.0-mas-x64.zip -87ad1b3868751b2af34cfebd62f9874ced837fc4389adad3e7659a437afcffb1 *ffmpeg-v42.2.0-win32-arm64.zip -57af529a0cc217bb5f20c67554eb526926b853a1f1594a06e8423662cead9c09 *ffmpeg-v42.2.0-win32-ia32.zip -62dc51e9290b444c6640048a715d8d7b60fe3e546a98f0faff945e4a522250f8 *ffmpeg-v42.2.0-win32-x64.zip -72ea4fdeabf3a49dd86bdeb136c916f7b54952b439edd527321963a5dbb0460d *hunspell_dictionaries.zip -98e8f208b8fd697e2a035d03e83de5731ba92b23f13f826abc60c0b7eab0d952 *libcxx-objects-v42.2.0-linux-arm64.zip -7187c3f0406a04b8e19af6ca374e6d7ca7833e47ea2382f7fba1ed7061f7d8be *libcxx-objects-v42.2.0-linux-armv7l.zip -7254b02f7220aa47210c382f45668016710d07cd7c7c61da5c4c556cc2334446 *libcxx-objects-v42.2.0-linux-x64.zip -37ade3096c7362b9df8945311f55abb8d039f93aabacec086e449cee4a31dcab *libcxx_headers.zip -c623bfe37d755eb9020fc080d4ef0df799acf3439b7b588f9a62d9b6e3908b75 *libcxxabi_headers.zip -44b16dd0e2bade1253e6f36235a43f005ed6472c1017195ee1ea1e629b8ab504 *mksnapshot-v42.2.0-darwin-arm64.zip -9d59364581e6001d655cd6c659e76ab80799d794e22b0c990b181268faa0492c *mksnapshot-v42.2.0-darwin-x64.zip -6035f7f148470352c04f0591d360685724a7784780f6d74c4fdaa99c71b8be6f *mksnapshot-v42.2.0-linux-arm64-x64.zip -27d4c39086c376744bd606f55c7b6c1420d0267f94cd8974031ce4b1f88320e6 *mksnapshot-v42.2.0-linux-armv7l-x64.zip -ca549469050d6a360e6b95ea202cb61ea704db51f7d67aa67c457e5dfa98b459 *mksnapshot-v42.2.0-linux-x64.zip -8d2271bd6cb9a16d34e02dfc92efb1fd1a2908038165efc9aa161cf5e62dc7ae *mksnapshot-v42.2.0-mas-arm64.zip -ae8453efb615242dd27de97cf1f9c6b8f4c6ad9cfab5fc476671d8751c5ac319 *mksnapshot-v42.2.0-mas-x64.zip -f21cdde8044078ce14acd0c65af1e7cf999d78e6d40d467cc73f854c9e9ef481 *mksnapshot-v42.2.0-win32-arm64-x64.zip -4b5b7d68067f55a0fc4dd4c295ca115070f288684e7942e0e80badc2b64754a6 *mksnapshot-v42.2.0-win32-ia32.zip -25d850c5674e0a821bb5897197b6e38c0d55a5cf0505d4d79a39b608b54c4b5c *mksnapshot-v42.2.0-win32-x64.zip +f46a52e829e4e8d294887346e2cd65a43cda3cf0c4988f9e8c40bff083b21ca0 *chromedriver-v42.5.0-darwin-arm64.zip +95ea8ac2949531f92a69e935345fb5f2c49dabb4d03244e90431b54e4177d66c *chromedriver-v42.5.0-darwin-x64.zip +217eff942b1fb63767a2fccdf309b8af67bc85dcbe5b6a626159706f8b0f6549 *chromedriver-v42.5.0-linux-arm64.zip +af76c74bebd454a43e6ed463eba64b2d635661203c31bca8efbe829169f1df2a *chromedriver-v42.5.0-linux-armv7l.zip +ada1322977b76a42c97baa12a9388fb3d33e6043a9d4380ded3ccd003c9f3481 *chromedriver-v42.5.0-linux-x64.zip +9a1fc333f04178abc4985596ff5e6843e857d63bc05be8ae9b8d39adc184890c *chromedriver-v42.5.0-mas-arm64.zip +7c1357049a4f4d87e86f80f002b948dbd77f2987002c05d09f67495ae38d6235 *chromedriver-v42.5.0-mas-x64.zip +619bb88746f6708d9f291847ec8f9a360b9dd799cdda96c2125e6e64f6ba5dc6 *chromedriver-v42.5.0-win32-arm64.zip +d3cf1b135f8fd9c27d5736f64b4afd1044b851ba3122c7a0985188aba94f2df0 *chromedriver-v42.5.0-win32-ia32.zip +a22ffa5f552adf98609ce15511133f8d3d2a0dee3f7be21cbf252bac98364ef6 *chromedriver-v42.5.0-win32-x64.zip +9600be1460e73162fa0bc04b0d27f6af9f87af9c9e8c1979feeb81777a984fce *electron-api.json +d32ee862313327486bfe33c2d91d404d4ee20146773c8a5f87bd418f8a8e9a91 *electron-v42.5.0-darwin-arm64-dsym-snapshot.zip +606a47375b0e48343d3a96d3bada5add9e26768036edecbdb6e6f1aea754e55e *electron-v42.5.0-darwin-arm64-dsym.tar.xz +155e135264dfe1b4109140acc23e321e00495516089937ee0909ff3e70a9447f *electron-v42.5.0-darwin-arm64-symbols.zip +5e392f66b3f6d13caa6d50bbe10fce3d848ca16c5680529357aa26c5da58a1e6 *electron-v42.5.0-darwin-arm64.zip +7a4691f6648a10dfb3315a45e1ce4c297a606d1c475b65fbb5c9d80688cbb0bb *electron-v42.5.0-darwin-x64-dsym-snapshot.zip +fc620d7787958dbe74dc9acaf42fe35fcdbba7f0c727b376e979b1b86e0d99c9 *electron-v42.5.0-darwin-x64-dsym.tar.xz +9fca997d9f6187b398730b69819e3643dc182b869e1ad0b80a7b5e1cc55a9ad1 *electron-v42.5.0-darwin-x64-symbols.zip +ca4792d47e74a6afce1d39a019f201d1a47fa81fcf4e8bcaac6fe5756f172940 *electron-v42.5.0-darwin-x64.zip +f144b1025a7b8c5bf34cfbc6f17f47e594db186de4b5647286d40618c8efd5c4 *electron-v42.5.0-linux-arm64-debug.zip +54878afd06169b12e1e2908319883d2e3ecd4a3fc39187152a4110f13a20db46 *electron-v42.5.0-linux-arm64-symbols.zip +0188f1d86efe785e5721cfaa1ae51d6ef68260705c30037d0d271aaaf78af315 *electron-v42.5.0-linux-arm64.zip +d58cf9b92c9e74885d6fd0ea0740602fa368d87967ed35b8286aaf4170e053e1 *electron-v42.5.0-linux-armv7l-debug.zip +45cfe194be6c3af8c8f4f04e83d26cea2f0d01d822d6d0d39cf24ef11298b6f3 *electron-v42.5.0-linux-armv7l-symbols.zip +360ff6d128be77be83fb3eaf458380fdca226be06f247551049fe3b6ef8ac382 *electron-v42.5.0-linux-armv7l.zip +a0fad46643b7bf6bfce7b2d0a3bd26166079e81e0581d9f3420484984e135c4b *electron-v42.5.0-linux-x64-debug.zip +3c1a4dc6c79bad3d9bc7d3af17b22bc3c97543c27a49b18d852b9eeb3ced2c0a *electron-v42.5.0-linux-x64-symbols.zip +6705a9d0cc5c8f225d705d6e1c2607b2b5be8667d2befb18cdafe8b7b29b8008 *electron-v42.5.0-linux-x64.zip +11b3c08d04f8e89983bff6ee1fc872f8d94f0a92c07311c81da6ac493cbedcc9 *electron-v42.5.0-mas-arm64-dsym-snapshot.zip +1ed2d9702069c699e143366d86e24398349072209459d23883c8bda74f62a161 *electron-v42.5.0-mas-arm64-dsym.tar.xz +7a474a3498973aeb9344c193a383e3590fde4ce083b0f0d51a0eced4f6d31ac6 *electron-v42.5.0-mas-arm64-symbols.zip +33166a8b868cd6624cb0d70847b42067219a2f532050d1cdd128d8d7eb9f2b74 *electron-v42.5.0-mas-arm64.zip +b6eaf6f40e20f196974f675c936a2d25c97a3f660faecba8978fd59b838a7b40 *electron-v42.5.0-mas-x64-dsym-snapshot.zip +30ca0601ba38e804152d16805eb01b05a60452359ce3d6b9cebb1e21d024f4dc *electron-v42.5.0-mas-x64-dsym.tar.xz +5da55d91f5b4159f4bc6fa4831b995b621dcab6ef60d20d64aaf074dcba3943c *electron-v42.5.0-mas-x64-symbols.zip +b2c23925f3d6561dadc8f563576b7a222ea20606a8002986e5937bd066710870 *electron-v42.5.0-mas-x64.zip +d7cea2c82487a4af809c181c99cec529d0a00c86fcfc70684d9a2c5c0d295212 *electron-v42.5.0-win32-arm64-pdb.zip +2799cffe27425316d27336fc03d8c77581aa8e65e27964104da68d7780b1a040 *electron-v42.5.0-win32-arm64-symbols.zip +8e245a0179165d88b82f382dda3f68d0851a2f4b21b14b833b9b7c4374d7da3a *electron-v42.5.0-win32-arm64-toolchain-profile.zip +765370134fdd2dd851bb019d7b7d9379ed7087f288eb8bee4043b42cfd9c33d8 *electron-v42.5.0-win32-arm64.zip +d4b34eb9e4e1ac77f11a0be51d39e94548362d1df63f6b7675016e6cb7b9b904 *electron-v42.5.0-win32-ia32-pdb.zip +eaa1fa1bc6bb14f8877e8ec930b90d9cdbc9dc41765cf530f2c5e6aca1da1fe8 *electron-v42.5.0-win32-ia32-symbols.zip +8e245a0179165d88b82f382dda3f68d0851a2f4b21b14b833b9b7c4374d7da3a *electron-v42.5.0-win32-ia32-toolchain-profile.zip +aa040c36074046a723fca9173aac1c8bf70c19d8a156215e8dbeb17ea37f588b *electron-v42.5.0-win32-ia32.zip +615726602fd5d697c12fc9b66de28343919631b24ab1179d83da46508e8a87d4 *electron-v42.5.0-win32-x64-pdb.zip +a913f632a979b7b7fb964b4d43ad45976638a5673cec1b6a69fd6572f57adc0d *electron-v42.5.0-win32-x64-symbols.zip +8e245a0179165d88b82f382dda3f68d0851a2f4b21b14b833b9b7c4374d7da3a *electron-v42.5.0-win32-x64-toolchain-profile.zip +127bbf7a755b438612c076b22baee258a87cd3d07168cc82ea46ffc015936114 *electron-v42.5.0-win32-x64.zip +21ead4c9c384849887ca8cac0a2a7a683d0cf90a3b62a2a2ba8519224e16e249 *electron.d.ts +45d8022f3bd67995d90242c5176c832fdf3abbdaac575b2469ca0537d0eb58db *ffmpeg-v42.5.0-darwin-arm64.zip +3a5e8f19fd4aa38b3d31eeb7da40f0434e87703050912fc0ca9dbca2de06f0e2 *ffmpeg-v42.5.0-darwin-x64.zip +9efdc919ffd357f00f080a8b1e1baeea19dcfc3164ea02d5cd12b93f85e67b73 *ffmpeg-v42.5.0-linux-arm64.zip +2e24581796500f7d6ed0c16076d74b7fe0b7cf625844623535077df03886bc26 *ffmpeg-v42.5.0-linux-armv7l.zip +78e8d6fd7432e6cbf4ef024728b46aa0b7fa292a598fbbadcc42474f87380d42 *ffmpeg-v42.5.0-linux-x64.zip +28e44748d48b6168aefde8601225c5f3dfb9404d616de2e6035c634354e0a6ac *ffmpeg-v42.5.0-mas-arm64.zip +0bdb9e4050c4159935e8640d0facd6e868583d921386e9c2a5bf3c550c5baf3c *ffmpeg-v42.5.0-mas-x64.zip +81a1c0273c172f140fbf261c194d7ae7369a1e7abe27cae413c8590e2bca4cad *ffmpeg-v42.5.0-win32-arm64.zip +87cffd235b85029c21a1e962132b608209e9929d2a22611985fda317cfe7e95f *ffmpeg-v42.5.0-win32-ia32.zip +497790a714fffc090d217b66332dfe6896609aa24293dc3fb4effce57fe29c82 *ffmpeg-v42.5.0-win32-x64.zip +6db69261ec30c95e30cc3bcd526e8fcec59cd1ad785a7985e520f77bc974a03c *hunspell_dictionaries.zip +441e5c5d7ac9407438d1f2a9974bbc0ef1c3a28e9408051f577e790cd6c6d171 *libcxx-objects-v42.5.0-linux-arm64.zip +99a3a1f895debddf023f01631c2c2b81911992bbbc514718d2ad24599040e4da *libcxx-objects-v42.5.0-linux-armv7l.zip +c7e4c5c5ac5505f97a555cc21bd09014a2e740e5d25e999f2bdc7542a86256e0 *libcxx-objects-v42.5.0-linux-x64.zip +92cdadcc04957db6e7b65421790f77f56bc0c8e6dc1407ad1adbfbdf83dbbc06 *libcxx_headers.zip +a169c9558951055efc054d31c3b4c8a04aa588ef1533b4456b6b27950d121994 *libcxxabi_headers.zip +9611bdee31d4464a8b06e9d44e99adf290a898969a502cfc9ac5616ae1026a0c *mksnapshot-v42.5.0-darwin-arm64.zip +89f7e234802958438a115c96b2f6207153f776a13b07247947fd2e5ca9b1ce3d *mksnapshot-v42.5.0-darwin-x64.zip +2a6d92eb9eb89419dc3b2ab1287329acc16955a62833d32bda2bd90ded45a5b0 *mksnapshot-v42.5.0-linux-arm64-x64.zip +391263da6782b2536067113ce7dab8ef19d9b3d59a74430fef59a4d8a7364502 *mksnapshot-v42.5.0-linux-armv7l-x64.zip +f7752a92ad45041e0c00a74bdc10a0d13574849ad944ba2b3424dc255460465f *mksnapshot-v42.5.0-linux-x64.zip +b9425403b15e273be565d7a937c7d28fc09f421df9fa4d376d6932bfecc91ff5 *mksnapshot-v42.5.0-mas-arm64.zip +a7b7230be427b849f31467298c2c4a934c7273e731ea17513c5790c6d66c8205 *mksnapshot-v42.5.0-mas-x64.zip +4a32b6558fb9aaae703cdc2e6a87ce05e95184e6d0e49e350a956c0908724ef7 *mksnapshot-v42.5.0-win32-arm64-x64.zip +ca73fb8ef0d6e4cf92f569a0dee9770f13166e67be68b12688a7ecaa630a2163 *mksnapshot-v42.5.0-win32-ia32.zip +d8a484f4f3347d7fd73bc4af59ce743e90e14fe7743a1be5ea09452e9c3bbd5b *mksnapshot-v42.5.0-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 16e83546f49a1..a787f4a19a9f9 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,6 +1,6 @@ -372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4 node-v24.15.0-darwin-arm64.tar.gz -ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b node-v24.15.0-darwin-x64.tar.gz -73afc234d558c24919875f51c2d1ea002a2ada4ea6f83601a383869fefa64eed node-v24.15.0-linux-arm64.tar.gz -44836872d9aec49f1e6b52a9a922872db9a2b02d235a616a5681b6a85fec8d89 node-v24.15.0-linux-x64.tar.gz -49a54c103f4919ce64199a043ef5cd309507de491d718085edee089cd8e87543 win-arm64/node.exe -3331e1ffe19874215472217c5e94f5a0c6d8e18c4ac7111d3937aa0ad5e9b4a5 win-x64/node.exe +4fc3266a3702eebc39cc37661cf4eeceeade307e242ab64e4d7ce7949197e11f node-v24.17.0-darwin-arm64.tar.gz +80da552fe037290cb130e9dea590f5eeeb7aa450636f0c89ab41415511c1ec27 node-v24.17.0-darwin-x64.tar.gz +faa0d59ba7fe7045c950ed09b190578fb8eee73e4358686d38fcc99ca58c1480 node-v24.17.0-linux-arm64.tar.gz +e0472427aa791ad80bdc426ff7cc73cdd28ed0f616d1ff9689a23a7f47f1265f node-v24.17.0-linux-x64.tar.gz +44999f9ec6486d01202d8961f343eac8c9f2847b234a8637c3fd0f1e2bb3288a win-arm64/node.exe +c6335d08331c23d68b9f2b18adb102002d76ef150b47248e954c507e0d033664 win-x64/node.exe diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 459bd55b3500a..7b6910a05e0e2 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -121,7 +121,7 @@ async function main(buildDir?: string) { outAppPath, force: true, mergeASARs: true, - x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node,**/node_modules/@github/copilot-darwin-*/copilot,**/node_modules/@github/copilot/prebuilds/darwin-*/*,**/node_modules/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot-darwin-*/copilot,**/node_modules.asar.unpacked/@github/copilot/prebuilds/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/prebuilds/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules/@vscode/ripgrep-universal/bin/darwin-*/*,**/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/darwin-*/*,**/node_modules/@microsoft/mxc-sdk/bin/**,**/node_modules.asar.unpacked/@microsoft/mxc-sdk/bin/**}', + x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node,**/node_modules/@github/copilot-darwin-*/**,**/node_modules/@github/copilot/prebuilds/darwin-*/*,**/node_modules/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot-darwin-*/**,**/node_modules.asar.unpacked/@github/copilot/prebuilds/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/prebuilds/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules/@vscode/ripgrep-universal/bin/darwin-*/*,**/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/darwin-*/*,**/node_modules/@microsoft/mxc-sdk/bin/**,**/node_modules.asar.unpacked/@microsoft/mxc-sdk/bin/**}', filesToSkipComparison: (file: string) => { for (const expected of filesToSkip) { if (minimatch(file, expected)) { diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index 62c30da5216af..caf85b4d72c3b 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -26,10 +26,12 @@ import { compileBuildWithManglingTask } from './gulpfile.compile.ts'; import { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileExtensionMediaBuildTask, compileCopilotExtensionBuildTask } from './gulpfile.extensions.ts'; import { vscodeWebResourceIncludes, createVSCodeWebFileContentMapper } from './gulpfile.vscode.web.ts'; import * as cp from 'child_process'; +import crypto from 'crypto'; import log from 'fancy-log'; import buildfile from './buildfile.ts'; -import { fetchUrls, fetchGithub } from './lib/fetch.ts'; -import { getCopilotExcludeFilter, getCopilotRuntimePrebuildFiles, getCopilotTgrepExcludeFilter, getRipgrepExcludeFilter, prepareBuiltInCopilotRipgrepShim } from './lib/copilot.ts'; +import { fetchUrls } from './lib/fetch.ts'; +import { downloadFeedPackage } from './lib/azureFeed.ts'; +import { ensureCopilotPlatformPackage, getCopilotExcludeFilter, getCopilotRuntimePrebuildFiles, getCopilotTgrepExcludeFilter, getRipgrepExcludeFilter, prepareBuiltInCopilotRipgrepShim } from './lib/copilot.ts'; import { readAgentSdkResults } from './agent-sdk/common.ts'; @@ -208,6 +210,44 @@ function patchElfLoadAlign(): NodeJS.ReadWriteStream { const { nodeVersion, internalNodeVersion } = getNodeVersion(); +// In product builds, the server (reh) Node.js binaries are fetched on demand +// from our Azure Artifacts feed named by `product.nodejsArtifactFeed` using the +// `az` CLI, instead of from nodejs.org (which is used by OSS builds when no feed +// is configured). Each universal package contains exactly one file, named after +// the asset minus its last extension, lowercased and sanitized (e.g. +// `node-v24.15.0-linux-x64.tar`, `win-x64-node`). +const nodejsArtifactFeed = product.nodejsArtifactFeed; + +function internalNodeFeedPackageName(assetName: string): string { + return assetName + .replace(/\.[^.]+$/, '') + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/^[._-]+/, '') + .replace(/[._-]+$/, ''); +} + +function fetchNodejsFromInternalFeed(feed: string, assetName: string, version: string, checksumSha256: string | undefined): NodeJS.ReadWriteStream { + const result = es.through(); + (async () => { + try { + const filePath = await downloadFeedPackage(REPO_ROOT, 'nodejs-feed', { feed, name: internalNodeFeedPackageName(assetName), version }); + const contents = await fs.promises.readFile(filePath); + if (checksumSha256) { + const actual = crypto.createHash('sha256').update(contents).digest('hex'); + if (actual !== checksumSha256) { + throw new Error(`Checksum mismatch for ${assetName} (expected ${checksumSha256}, actual ${actual})`); + } + } + result.emit('data', new File({ path: path.basename(filePath), contents })); + result.emit('end'); + } catch (err) { + result.emit('error', err); + } + })(); + return result; +} + BUILD_TARGETS.forEach(({ platform, arch }) => { task.task(task.define(`node-${platform}-${arch}`, () => { const nodePath = path.join('.build', 'node', `v${nodeVersion}`, `${platform}-${arch}`); @@ -237,13 +277,13 @@ function nodejs(platform: string, arch: string): NodeJS.ReadWriteStream | undefi arch = 'x64'; } - log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from ${product.nodejsRepository}...`); + log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from ${nodejsArtifactFeed || 'https://nodejs.org'}...`); const glibcPrefix = process.env['VSCODE_NODE_GLIBC'] ?? ''; let expectedName: string | undefined; switch (platform) { case 'win32': - expectedName = product.nodejsRepository !== 'https://nodejs.org' ? + expectedName = nodejsArtifactFeed ? `win-${arch}-node.exe` : `win-${arch}/node.exe`; break; @@ -267,14 +307,14 @@ function nodejs(platform: string, arch: string): NodeJS.ReadWriteStream | undefi switch (platform) { case 'win32': - return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) : + return (nodejsArtifactFeed ? + fetchNodejs(expectedName!, checksumSha256) : fetchUrls(`/dist/v${nodeVersion}/win-${arch}/node.exe`, { base: 'https://nodejs.org', checksumSha256 })) .pipe(rename('node.exe')); case 'darwin': case 'linux': { - const downloaded = (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) : + const downloaded = (nodejsArtifactFeed ? + fetchNodejs(expectedName!, checksumSha256) : fetchUrls(`/dist/v${nodeVersion}/node-v${nodeVersion}-${platform}-${arch}.tar.gz`, { base: 'https://nodejs.org', checksumSha256 }) ).pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) @@ -283,8 +323,8 @@ function nodejs(platform: string, arch: string): NodeJS.ReadWriteStream | undefi return platform === 'linux' && arch === 'x64' ? downloaded.pipe(patchElfLoadAlign()) : downloaded; } case 'alpine': - return product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) + return nodejsArtifactFeed ? + fetchNodejs(expectedName!, checksumSha256) .pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) .pipe(util.setExecutableBit('**')) @@ -293,6 +333,13 @@ function nodejs(platform: string, arch: string): NodeJS.ReadWriteStream | undefi } } +// Fetches a server (reh) Node.js asset from the Azure Artifacts feed named by +// `product.nodejsArtifactFeed`. Only called when that feed is configured. +function fetchNodejs(assetName: string, checksumSha256: string | undefined): NodeJS.ReadWriteStream { + const version = `${nodeVersion}-${internalNodeVersion}`; + return fetchNodejsFromInternalFeed(nodejsArtifactFeed, assetName, version, checksumSha256); +} + function packageTask(type: string, platform: string, arch: string, sourceFolderName: string, destinationFolderName: string) { const destination = path.join(BUILD_ROOT, destinationFolderName); @@ -395,6 +442,7 @@ function packageTask(type: string, platform: string, arch: string, sourceFolderN .pipe(filter(['**', '!**/package-lock.json', '!**/*.{js,css}.map'])) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))); + ensureCopilotPlatformPackage(platform, arch, 'remote/node_modules'); const copilotRuntimePrebuilds = gulp.src(getCopilotRuntimePrebuildFiles(platform, arch, 'remote/node_modules'), { base: 'remote', dot: true, allowEmpty: true }); const deps = es.merge(cleanedDeps, copilotRuntimePrebuilds) .pipe(filter(getCopilotExcludeFilter(platform, arch))) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 53987f88edb1a..02750e9a85610 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -28,7 +28,7 @@ import minimist from 'minimist'; import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from './gulpfile.compile.ts'; import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask, compileCopilotExtensionBuildTask } from './gulpfile.extensions.ts'; import { copyCodiconsTask } from './lib/compilation.ts'; -import { getCopilotExcludeFilter, getCopilotRuntimePrebuildFiles, getCopilotTgrepExcludeFilter, getRipgrepExcludeFilter, prepareBuiltInCopilotRipgrepShim } from './lib/copilot.ts'; +import { ensureCopilotPlatformPackage, getCopilotExcludeFilter, getCopilotRuntimePrebuildFiles, getCopilotTgrepExcludeFilter, getRipgrepExcludeFilter, prepareBuiltInCopilotRipgrepShim } from './lib/copilot.ts'; import { readAgentSdkResults } from './agent-sdk/common.ts'; import { useEsbuildTranspile } from './buildConfig.ts'; import { promisify } from 'util'; @@ -340,6 +340,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(filter(depFilterPattern)) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))); + ensureCopilotPlatformPackage(platform, arch); const copilotRuntimePrebuilds = gulp.src(getCopilotRuntimePrebuildFiles(platform, arch), { base: '.', dot: true, allowEmpty: true }); const deps = es.merge(cleanedDeps, copilotRuntimePrebuilds) .pipe(filter(getCopilotExcludeFilter(platform, arch))) diff --git a/build/lib/azureFeed.ts b/build/lib/azureFeed.ts new file mode 100644 index 0000000000000..f32fb21885daa --- /dev/null +++ b/build/lib/azureFeed.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import cp from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +function getEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} + +function azExecFile(args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = cp.spawn('az', args, { stdio: 'inherit', shell: process.platform === 'win32' }); + child.on('error', reject); + child.on('close', code => code === 0 ? resolve() : reject(new Error(`az ${args[0]} ${args[1] ?? ''} exited with code ${code}`))); + }); +} + +let azureDevOpsExtension: Promise | undefined; +function ensureAzureDevOpsExtension(): Promise { + if (!azureDevOpsExtension) { + azureDevOpsExtension = (async () => { + const result = cp.spawnSync('az', ['extension', 'show', '--name', 'azure-devops'], { stdio: 'ignore', shell: process.platform === 'win32' }); + if (result.status !== 0) { + await azExecFile(['extension', 'add', '--name', 'azure-devops', '--only-show-errors']); + } + })(); + } + return azureDevOpsExtension; +} + +export interface IFeedPackage { + readonly feed: string; + readonly name: string; + readonly version: string; +} + +/** + * Downloads a universal package from an Azure Artifacts feed into a cache + * directory under `/.build/` and returns the absolute path to + * the single file it contains. Subsequent requests for the same package are + * served from the cache. + * @param root the repository root + * @param cacheDir the `.build` sub directory used to cache downloaded packages + * @param pkg the feed, package name and version to download + */ +export async function downloadFeedPackage(root: string, cacheDir: string, pkg: IFeedPackage): Promise { + const dir = path.join(root, '.build', cacheDir, `${pkg.name}-${pkg.version}`); + if (!fs.existsSync(dir)) { + await ensureAzureDevOpsExtension(); + await azExecFile([ + 'artifacts', 'universal', 'download', + '--organization', getEnv('SYSTEM_COLLECTIONURI').replace(/\/+$/, ''), + '--project', getEnv('SYSTEM_TEAMPROJECT'), + '--scope', 'project', + '--feed', pkg.feed, + '--name', pkg.name, + '--version', pkg.version, + '--path', dir, + ]); + } + const [only] = await fs.promises.readdir(dir); + return path.join(dir, only); +} diff --git a/build/lib/copilot.ts b/build/lib/copilot.ts index 74babc798f4e4..37faf2b066631 100644 --- a/build/lib/copilot.ts +++ b/build/lib/copilot.ts @@ -4,7 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; +import { createHash } from 'crypto'; +import { execFileSync } from 'child_process'; +import * as os from 'os'; import * as path from 'path'; +import { extract } from 'tar'; /** * The platforms that @github/copilot ships platform-specific packages for. @@ -13,6 +17,7 @@ import * as path from 'path'; export const copilotPlatforms = [ 'darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', + 'linuxmusl-arm64', 'linuxmusl-x64', 'win32-arm64', 'win32-x64', ]; @@ -73,6 +78,42 @@ function toCopilotTgrepPlatformArch(platform: string, arch: string): string { return `${nodePlatform}-${nodeArch}`; } +function toCopilotPackagePlatformArch(platform: string, arch: string): string { + if (platform === 'alpine') { + return `linuxmusl-${arch}`; + } + if (arch === 'alpine') { + return 'linuxmusl-x64'; + } + + const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); + return `${nodePlatform}-${nodeArch}`; +} + +const copilotOptionalNativePayloadDirs = [ + 'clipboard', + 'foundry-local-sdk', + 'mxc-bin', + 'pvrecorder', +]; + +function getCopilotOptionalNativePayloadFiles(platform: string): string[] { + const files = [ + 'prebuilds/*/computer.node', + 'prebuilds/*/computer-use-mcp', + 'prebuilds/*/computer-use-mcp.exe', + 'prebuilds/*/Copilot Computer Use.app/**', + 'prebuilds/*/CopilotComputerUse.exe', + 'prebuilds/*/keytar.node', + ]; + + if (platform !== 'win32') { + files.push('prebuilds/*/cli-native.node'); + } + + return files; +} + /** * Returns a glob filter that strips @vscode/ripgrep-universal bin directories * for architectures other than the build target. @@ -102,72 +143,142 @@ export function getCopilotTgrepExcludeFilter(platform: string, arch: string): st * Returns a glob filter that strips @github/copilot platform packages * for architectures other than the build target. * - * For platforms the copilot SDK doesn't natively support (e.g. alpine, armhf), - * ALL platform packages are stripped - that's fine because the copilot CLI SDK - * resolves `node-pty` from the embedder (VS Code) first via `hostRequire`, - * falling back to its bundled copy only if the embedder can't provide it. + * Alpine uses the linuxmusl-* packages. Other platform package names follow + * Node's `${process.platform}-${process.arch}` naming. If Copilot does not + * ship the computed platform package (for example linux-arm for armhf builds), + * this strips every known @github/copilot-* platform package. */ export function getCopilotExcludeFilter(platform: string, arch: string): string[] { - const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); - const targetPlatformArch = `${nodePlatform}-${nodeArch}`; + const targetPlatformArch = toCopilotPackagePlatformArch(platform, arch); const nonTargetPlatforms = copilotPlatforms.filter(p => p !== targetPlatformArch); // Strip wrong-architecture @github/copilot-{platform} packages. const excludes = nonTargetPlatforms.map(p => `!**/node_modules/@github/copilot-${p}/**`); - return ['**', ...excludes]; + return [ + '**', + ...excludes, + '!**/node_modules/@github/copilot-*/copilot', + '!**/node_modules/@github/copilot-*/copilot.exe', + ]; } /** - * Returns the public @github/copilot-sdk runtime native addon files that must - * survive app/remote packaging for the target platform. + * Returns the public @github/copilot package files that must survive + * app/remote packaging for the target platform. * - * .moduleignore strips @github/copilot/prebuilds/** globally because the - * internal extension SDK uses a copied sdk/prebuilds layout. Agent Host uses - * the public SDK, whose runtime addon loader expects runtime.node in the root - * prebuilds layout. The SDK's built-in shell tool additionally spawns commands - * through node-pty, whose native binaries live in the same root prebuilds - * layout, so those must be preserved too (otherwise the sandboxed shell fails - * with `Cannot find module './prebuilds//pty.node'` — or conpty.node - * on Windows). + * .moduleignore strips all @github/copilot-* platform packages globally. + * Re-add the selected runtime package so Agent Host can launch its index.js + * entrypoint and load runtime prebuilds. Keep the standalone SEA executable + * and optional native payload trees out of the product build. */ export function getCopilotRuntimePrebuildFiles(platform: string, arch: string, nodeModulesRoot = 'node_modules'): string[] { - const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); - const targetPlatformArch = `${nodePlatform}-${nodeArch}`; - const prebuildDir = path.posix.join(nodeModulesRoot, '@github', 'copilot', 'prebuilds', targetPlatformArch); + const copilotPackagePlatformArch = toCopilotPackagePlatformArch(platform, arch); + const copilotPlatformPackageDir = path.posix.join(nodeModulesRoot, '@github', `copilot-${copilotPackagePlatformArch}`); - const files = [ - path.posix.join(prebuildDir, 'runtime.node'), + return [ + path.posix.join(copilotPlatformPackageDir, '**'), + `!${path.posix.join(copilotPlatformPackageDir, 'copilot')}`, + `!${path.posix.join(copilotPlatformPackageDir, 'copilot.exe')}`, + ...copilotOptionalNativePayloadDirs.map(dir => `!${path.posix.join(copilotPlatformPackageDir, dir, '**')}`), + ...getCopilotOptionalNativePayloadFiles(platform).map(file => `!${path.posix.join(copilotPlatformPackageDir, file)}`), ]; +} - // node-pty native binaries for the SDK's built-in shell tool. Windows uses - // ConPTY (conpty.node plus the conpty/ helpers); darwin/linux use pty.node, - // and darwin additionally ships the spawn-helper executable. - if (nodePlatform === 'win32') { - files.push( - path.posix.join(prebuildDir, 'conpty.node'), - path.posix.join(prebuildDir, 'conpty_console_list.node'), - path.posix.join(prebuildDir, 'conpty', 'OpenConsole.exe'), - path.posix.join(prebuildDir, 'conpty', 'conpty.dll'), - ); - } else { - files.push(path.posix.join(prebuildDir, 'pty.node')); - if (nodePlatform === 'darwin') { - files.push(path.posix.join(prebuildDir, 'spawn-helper')); - } +interface NpmPackageLock { + packages?: Record; +} + +interface EnsureCopilotPlatformPackageOptions { + packPackage?: (packageName: string, version: string, tempDir: string) => string; +} + +/** + * Ensures the selected @github/copilot-{platform} package is present before + * packaging. npm only installs the host-compatible optional dependency, but + * VS Code packaging can cross-build targets such as darwin-x64 on arm64 hosts. + */ +export function ensureCopilotPlatformPackage(platform: string, arch: string, nodeModulesRoot = 'node_modules', options: EnsureCopilotPlatformPackageOptions = {}): void { + const copilotPackagePlatformArch = toCopilotPackagePlatformArch(platform, arch); + if (!copilotPlatforms.includes(copilotPackagePlatformArch)) { + return; } - return files; + const packageName = `@github/copilot-${copilotPackagePlatformArch}`; + const packageDir = path.join(nodeModulesRoot, '@github', `copilot-${copilotPackagePlatformArch}`); + if (fs.existsSync(packageDir)) { + return; + } + + const lockFilePath = path.join(path.dirname(nodeModulesRoot), 'package-lock.json'); + const lockPackageKey = path.posix.join('node_modules', '@github', `copilot-${copilotPackagePlatformArch}`); + const lockPackage = readNpmPackageLock(lockFilePath).packages?.[lockPackageKey]; + if (!lockPackage?.version) { + throw new Error(`[ensureCopilotPlatformPackage] Missing ${lockPackageKey} in ${lockFilePath}. Run npm install to refresh the lockfile.`); + } + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-copilot-platform-')); + try { + const tarballPath = (options.packPackage ?? packCopilotPlatformPackage)(packageName, lockPackage.version, tempDir); + verifyNpmIntegrity(tarballPath, lockPackage.integrity); + + fs.mkdirSync(packageDir, { recursive: true }); + extract({ file: tarballPath, cwd: packageDir, strip: 1, sync: true }); + console.log(`[ensureCopilotPlatformPackage] Materialized ${packageName}@${lockPackage.version} in ${packageDir}`); + } catch (err) { + fs.rmSync(packageDir, { recursive: true, force: true }); + throw new Error(`[ensureCopilotPlatformPackage] Failed to materialize ${packageName}@${lockPackage.version}: ${err instanceof Error ? err.message : String(err)}`); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} + +function packCopilotPlatformPackage(packageName: string, version: string, tempDir: string): string { + execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['pack', `${packageName}@${version}`, '--pack-destination', tempDir, '--silent'], { stdio: 'pipe', shell: process.platform === 'win32' }); + + const tarball = fs.readdirSync(tempDir).find(name => name.endsWith('.tgz')); + if (!tarball) { + throw new Error(`npm pack did not produce a tarball in ${tempDir}`); + } + + return path.join(tempDir, tarball); +} + +function readNpmPackageLock(lockFilePath: string): NpmPackageLock { + try { + return JSON.parse(fs.readFileSync(lockFilePath, 'utf8')); + } catch (err) { + throw new Error(`[ensureCopilotPlatformPackage] Failed to read ${lockFilePath}: ${err instanceof Error ? err.message : String(err)}`); + } +} + +function verifyNpmIntegrity(tarballPath: string, integrity: string | undefined): void { + if (!integrity) { + return; + } + + const sha512Integrity = integrity.split(/\s+/).find(entry => entry.startsWith('sha512-')); + if (!sha512Integrity) { + return; + } + + const expected = sha512Integrity.slice('sha512-'.length); + const actual = createHash('sha512').update(fs.readFileSync(tarballPath)).digest('base64'); + if (actual !== expected) { + throw new Error(`integrity mismatch for ${tarballPath}`); + } } /** - * Materializes the copilot CLI ripgrep shim directly inside the built-in copilot extension. + * Materializes target-platform Copilot CLI SDK files directly inside the built-in copilot extension. * * This is used when copilot is shipped as a built-in extension so startup does - * not need to create the shim at runtime. The destination layout matches the - * runtime shim logic in the copilot extension: - * - ripgrep: node_modules/@github/copilot/sdk/ripgrep/bin/{platform-arch} - * - marker: node_modules/@github/copilot/shims.txt + * not need to create the shim at runtime. The Copilot VSIX is built once on the + * Linux x64 host, so product packaging also restores target-platform SDK + * natives from the selected @github/copilot-{platform} package. * * Note: `node-pty` is no longer shimmed. The copilot CLI SDK resolves * `node-pty` from the embedder (VS Code) via `hostRequire` and falls back to @@ -179,6 +290,7 @@ export function getCopilotRuntimePrebuildFiles(platform: string, arch: string, n export function prepareBuiltInCopilotRipgrepShim(platform: string, arch: string, builtInCopilotExtensionDir: string, appNodeModulesDir: string): void { const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); const platformArch = `${nodePlatform}-${nodeArch}`; + const copilotPackagePlatformArch = toCopilotPackagePlatformArch(platform, arch); const tgrepPlatformArch = toCopilotTgrepPlatformArch(platform, arch); const extensionNodeModules = path.join(builtInCopilotExtensionDir, 'node_modules'); @@ -187,7 +299,8 @@ export function prepareBuiltInCopilotRipgrepShim(platform: string, arch: string, if (!fs.existsSync(copilotSdkBase)) { throw new Error(`[prepareBuiltInCopilotRipgrepShim] Copilot SDK directory not found at ${copilotSdkBase}`); } - pruneNonTargetCopilotSdkPrebuilds(platformArch, path.join(copilotSdkBase, 'prebuilds'), copilotPlatforms); + materializeBuiltInCopilotSdkPlatformFiles(copilotPackagePlatformArch, tgrepPlatformArch, copilotBase, appNodeModulesDir); + pruneNonTargetCopilotSdkPrebuilds(copilotPackagePlatformArch, path.join(copilotSdkBase, 'prebuilds'), copilotPlatforms); pruneNonTargetCopilotSdkPrebuilds(tgrepPlatformArch, path.join(copilotSdkBase, path.join('tgrep', 'bin')), copilotTgrepPlatforms); pruneNonTargetCopilotSdkPrebuilds(tgrepPlatformArch, path.join(copilotBase, path.join('tgrep', 'bin')), copilotTgrepPlatforms); @@ -219,6 +332,49 @@ export function prepareBuiltInCopilotRipgrepShim(platform: string, arch: string, } } +function materializeBuiltInCopilotSdkPlatformFiles(copilotPackagePlatformArch: string, tgrepPlatformArch: string, copilotBase: string, appNodeModulesDir: string): void { + if (!copilotPlatforms.includes(copilotPackagePlatformArch)) { + return; + } + + const platformPackageDir = path.join(appNodeModulesDir, '@github', `copilot-${copilotPackagePlatformArch}`); + if (!fs.existsSync(platformPackageDir)) { + throw new Error(`[prepareBuiltInCopilotRipgrepShim] Copilot platform package not found at ${platformPackageDir}`); + } + + copyRequiredDirectory( + path.join(platformPackageDir, 'prebuilds', copilotPackagePlatformArch), + path.join(copilotBase, 'sdk', 'prebuilds', copilotPackagePlatformArch), + `Copilot SDK native prebuilds for ${copilotPackagePlatformArch}` + ); + + if (!copilotTgrepPlatforms.includes(tgrepPlatformArch)) { + return; + } + + const tgrepSource = path.join(platformPackageDir, 'tgrep', 'bin', tgrepPlatformArch); + copyRequiredDirectory( + tgrepSource, + path.join(copilotBase, 'tgrep', 'bin', tgrepPlatformArch), + `Copilot tgrep for ${tgrepPlatformArch}` + ); + copyRequiredDirectory( + tgrepSource, + path.join(copilotBase, 'sdk', 'tgrep', 'bin', tgrepPlatformArch), + `Copilot SDK tgrep for ${tgrepPlatformArch}` + ); +} + +function copyRequiredDirectory(source: string, target: string, description: string): void { + if (!fs.existsSync(source)) { + throw new Error(`[prepareBuiltInCopilotRipgrepShim] ${description} not found at ${source}`); + } + + fs.rmSync(target, { recursive: true, force: true }); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.cpSync(source, target, { recursive: true }); +} + function pruneNonTargetCopilotSdkPrebuilds(targetPlatformArch: string, prebuildsDir: string, platformArchs: string[]): void { if (!fs.existsSync(prebuildsDir)) { return; diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 016c25f7553db..f9236eaf47235 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -5,10 +5,12 @@ import fs from 'fs'; import path from 'path'; +import { Readable } from 'stream'; import vfs from 'vinyl-fs'; import { filter, jsonEditor } from './gulp/facade.ts'; import * as util from './util.ts'; import { getVersion } from './getVersion.ts'; +import { downloadFeedPackage } from './azureFeed.ts'; import electron from '@vscode/gulp-electron'; type DarwinDocumentSuffix = 'document' | 'script' | 'file' | 'source code'; @@ -100,12 +102,45 @@ function darwinBundleDocumentTypes(types: { [name: string]: string | string[] }, }); } -const { msBuildId } = util.getElectronVersion(); -const electronVersion = '42.2.0'; +const { electronVersion, msBuildId } = util.getElectronVersion(); + +// In product builds, `@vscode/gulp-electron` is given an asset resolver (via the +// `repo` option) that fetches the prebuilt Electron archives on demand from the +// Azure Artifacts feed named by `product.electronArtifactFeed` using the `az` +// CLI, instead of downloading them from electron's official GitHub releases +// (which OSS builds use when no feed is configured). Each universal package +// contains exactly one file, which is streamed back as a `Response` and +// validated against the feed's `SHASUMS256.txt`. +const electronFeed: string | undefined = product.electronArtifactFeed; + +// Maps the artifact file name `@vscode/gulp-electron` requests to the matching +// universal package name in the feed, or `undefined` when it is not mirrored. +function feedPackageName(fileName: string): string | undefined { + if (fileName === 'SHASUMS256.txt') { + return 'shasums256'; + } + if (fileName.endsWith('-symbols.zip')) { + return undefined; + } + return fileName.replace(/\.zip$/, ''); +} + +const electronAssetResolver = electronFeed + ? async ({ fileName }: { url: string; fileName: string }): Promise => { + const name = feedPackageName(fileName); + if (!name) { + return new Response(null, { status: 404 }); + } + const version = `${electronVersion}-${msBuildId}`; + const filePath = await downloadFeedPackage(root, 'electron-feed', { feed: electronFeed, name, version }); + const size = (await fs.promises.stat(filePath)).size; + const body = Readable.toWeb(fs.createReadStream(filePath)) as ReadableStream; + return new Response(body, { status: 200, headers: { 'Content-Length': String(size) } }); + } + : undefined; export const config = { version: electronVersion, - tag: product.electronRepository ? `v${electronVersion}-${msBuildId}` : undefined, productAppName: product.nameLong, companyName: 'Microsoft Corporation', copyright: 'Copyright (C) 2026 Microsoft. All rights reserved', @@ -203,7 +238,7 @@ export const config = { linuxExecutableName: product.applicationName, winIcon: 'resources/win32/code.ico', token: process.env['GITHUB_TOKEN'], - repo: product.electronRepository || undefined, + repo: electronAssetResolver, validateChecksum: true, checksumFile: path.join(root, 'build', 'checksums', 'electron.txt'), createVersionedResources: useVersionedUpdate, diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index e98614e34386f..05de99a03a982 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -306,6 +306,10 @@ "name": "vs/workbench/contrib/welcomeOnboarding", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/onboarding", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/welcomePage", "project": "vscode-workbench" @@ -640,6 +644,10 @@ "name": "vs/sessions", "project": "vscode-sessions" }, + { + "name": "vs/sessions/contrib/automations", + "project": "vscode-sessions" + }, { "name": "vs/sessions/contrib/accountMenu", "project": "vscode-sessions" @@ -704,6 +712,14 @@ "name": "vs/sessions/contrib/git", "project": "vscode-sessions" }, + { + "name": "vs/sessions/contrib/github", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/layout", + "project": "vscode-sessions" + }, { "name": "vs/sessions/contrib/logs", "project": "vscode-sessions" @@ -716,6 +732,10 @@ "name": "vs/sessions/contrib/sessions", "project": "vscode-sessions" }, + { + "name": "vs/sessions/contrib/sessionInputBanners", + "project": "vscode-sessions" + }, { "name": "vs/sessions/contrib/terminal", "project": "vscode-sessions" @@ -739,6 +759,10 @@ { "name": "vs/sessions/contrib/editor", "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/onboardingTours", + "project": "vscode-sessions" } ] } diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index 02f95ca785c66..f82aff27a42d7 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -38,6 +38,66 @@ } ], "policies": [ + { + "key": "chat.agentHost.otel.otlpProtocol", + "name": "CopilotOtelOtlpProtocol", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.otlpProtocol.policy", + "value": "Controls the enterprise-managed OTLP wire protocol (protobuf vs JSON) for Copilot OpenTelemetry export." + } + }, + "type": "string", + "default": "", + "included": false + }, + { + "key": "chat.agentHost.otel.serviceName", + "name": "CopilotOtelServiceName", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.serviceName.policy", + "value": "Controls the enterprise-managed OTel `service.name` resource attribute for Copilot OpenTelemetry export." + } + }, + "type": "string", + "default": "", + "included": false + }, + { + "key": "chat.agentHost.otel.resourceAttributes", + "name": "CopilotOtelResourceAttributes", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.resourceAttributes.policy", + "value": "Controls the enterprise-managed OTel resource attributes for Copilot OpenTelemetry export." + } + }, + "type": "object", + "default": {}, + "included": false + }, + { + "key": "chat.agentHost.otel.headers", + "name": "CopilotOtelHeaders", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.headers.policy", + "value": "Controls the enterprise-managed OTLP exporter headers for Copilot OpenTelemetry export." + } + }, + "type": "object", + "default": {}, + "included": false + }, { "key": "mcp.enterpriseManagedAuth.idp", "name": "McpEnterpriseManagedAuthIdp", @@ -46,7 +106,7 @@ "localization": { "description": { "key": "mcp.enterpriseManagedAuth.idp.policy", - "value": "The OAuth/OIDC IdP configuration used for enterprise-managed Model Context Protocol (MCP) server authentication. Delivered through enterprise policy (Windows Group Policy, macOS managed preferences, Linux `/etc/vscode/policy.json`)." + "value": "The OAuth/OIDC IdP configuration used for enterprise-managed Model Context Protocol (MCP) server authentication." } }, "type": "object", @@ -143,6 +203,24 @@ "default": true, "included": true }, + { + "key": "chat.agentHost.claudeAgent.enabled", + "name": "Claude3PIntegration", + "category": "InteractiveSession", + "minimumVersion": "1.113", + "localization": { + "description": { + "key": "chat.agentHost.claudeAgent.enabled.policy", + "value": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic Claude Agent SDK directly in the editor. Uses your existing Copilot subscription." + } + }, + "type": "boolean", + "default": true, + "included": true, + "referencedSettings": [ + "github.copilot.chat.claudeAgent.enabled" + ] + }, { "key": "chat.agentHost.codexAgent.enabled", "name": "Codex3PIntegration", @@ -158,6 +236,120 @@ "default": false, "included": true }, + { + "key": "chat.agentHost.otel.enabled", + "name": "CopilotOtelEnabled", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.enabled.policy", + "value": "Controls whether Copilot OpenTelemetry export is enabled. When managed, users cannot override the enterprise value." + } + }, + "type": "boolean", + "default": false, + "included": true + }, + { + "key": "chat.agentHost.otel.exporterType", + "name": "CopilotOtelProtocol", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.protocol.policy", + "value": "Controls the enterprise-managed OTLP protocol for Copilot OpenTelemetry export." + }, + "enumDescriptions": [ + { + "key": "chat.agentHost.otel.protocol.policy.otlpHttp", + "value": "Use OTLP over HTTP." + }, + { + "key": "chat.agentHost.otel.protocol.policy.otlpGrpc", + "value": "Use OTLP over gRPC." + }, + { + "key": "chat.agentHost.otel.protocol.policy.console", + "value": "Console exporter is not selected by enterprise managed settings." + }, + { + "key": "chat.agentHost.otel.protocol.policy.file", + "value": "File exporter is not selected by enterprise managed settings." + } + ] + }, + "type": "string", + "default": "otlp-http", + "enum": [ + "otlp-http", + "otlp-grpc", + "console", + "file" + ], + "included": true + }, + { + "key": "chat.agentHost.otel.otlpEndpoint", + "name": "CopilotOtelEndpoint", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.otlpEndpoint.policy", + "value": "Controls the enterprise-managed OTLP collector endpoint for Copilot OpenTelemetry export." + } + }, + "type": "string", + "default": "", + "included": true + }, + { + "key": "chat.agentHost.otel.captureContent", + "name": "CopilotOtelCaptureContent", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.captureContent.policy", + "value": "Controls whether Copilot OpenTelemetry export captures prompt, response, and tool content." + } + }, + "type": "boolean", + "default": false, + "included": true + }, + { + "key": "chat.agentHost.otel.outfile", + "name": "CopilotOtelOutfile", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.outfile.policy", + "value": "Prevents local file export when enterprise-managed Copilot OpenTelemetry export is configured." + } + }, + "type": "string", + "default": "", + "included": true + }, + { + "key": "chat.defaultModel", + "name": "ChatDefaultModel", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.defaultModel.policy", + "value": "Sets the default chat model for new conversations. Accepts \"auto\", a model family name (such as \"opus\" or \"gemini\"), or a full model id. Users can still switch the model within a conversation." + } + }, + "type": "string", + "default": "", + "included": true + }, { "key": "chat.tools.global.autoApprove", "name": "ChatToolsAutoApprove", @@ -338,7 +530,7 @@ "localization": { "description": { "key": "chat.agent.allowedNetworkDomains", - "value": "Allowed domains for network access by agent tools (fetch tool, integrated browser). Applies when `#chat.agent.networkFilter#` or `#chat.agent.sandbox.enabled#` is enabled. When `#chat.agent.sandbox.enabled#` is set to `allowNetwork`, all domains are allowed. Supports wildcards like `*.example.com`. When both allowed and denied lists are empty, all domains are blocked. Denied domains (see `#chat.agent.deniedNetworkDomains#`) take precedence." + "value": "Allowed domains for network access by agent tools (fetch tool, integrated browser). Applies when `#chat.agent.networkFilter#` or `#chat.agent.sandbox.enabled#` is enabled. When `#chat.agent.sandbox.allowNetwork#` is enabled, all domains are allowed. Supports wildcards like `*.example.com`. When both allowed and denied lists are empty, all domains are blocked. Denied domains (see `#chat.agent.deniedNetworkDomains#`) take precedence." } }, "type": "array", @@ -353,7 +545,7 @@ "localization": { "description": { "key": "chat.agent.deniedNetworkDomains", - "value": "Denied domains for network access by agent tools (fetch tool, integrated browser). Applies when `#chat.agent.networkFilter#` or `#chat.agent.sandbox.enabled#` is enabled. This does not apply when `#chat.agent.sandbox.enabled#` is set to `allowNetwork`. Takes precedence over `#chat.agent.allowedNetworkDomains#`. Supports wildcards like `*.example.com`." + "value": "Denied domains for network access by agent tools (fetch tool, integrated browser). Applies when `#chat.agent.networkFilter#` or `#chat.agent.sandbox.enabled#` is enabled. This does not apply when `#chat.agent.sandbox.allowNetwork#` is enabled. Takes precedence over `#chat.agent.allowedNetworkDomains#`. Supports wildcards like `*.example.com`." } }, "type": "array", @@ -442,7 +634,7 @@ "localization": { "description": { "key": "agentSandbox.enabledSetting", - "value": "Controls whether agent mode uses sandboxing to restrict what tools can do. When enabled, tools like the terminal are run in a sandboxed environment to limit access to the system." + "value": "Controls whether agent mode uses sandboxing to restrict what tools can do. When enabled, tools like the terminal are run in a sandboxed environment to limit access to the system. Use `#chat.agent.sandbox.allowNetwork#` to allow all network domains." }, "enumDescriptions": [ { @@ -452,10 +644,6 @@ { "key": "agentSandbox.enabledSetting.onDescription", "value": "Enable sandboxing for agent mode tools." - }, - { - "key": "agentSandbox.enabledSetting.allowNetworkDescription", - "value": "Enable sandboxing for agent mode tools and allow all network domains." } ] }, @@ -463,11 +651,25 @@ "default": "off", "enum": [ "off", - "on", - "allowNetwork" + "on" ], "included": true }, + { + "key": "chat.agent.sandbox.allowNetwork", + "name": "ChatAgentSandboxAllowNetwork", + "category": "IntegratedTerminal", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "agentSandbox.allowNetwork", + "value": "When `#chat.agent.sandbox.enabled#` is enabled, controls whether to allow all network domains in the sandbox. When enabled, the sandbox preserves file system restrictions while relaxing all network restrictions." + } + }, + "type": "boolean", + "default": false, + "included": true + }, { "key": "chat.agent.sandbox.allowUnsandboxedCommands", "name": "ChatAgentSandboxAllowUnsandboxedCommands", @@ -506,7 +708,7 @@ "localization": { "description": { "key": "updateMode", - "value": "Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service." + "value": "Configure whether you receive automatic updates. The updates are fetched from a Microsoft online service." }, "enumDescriptions": [ { @@ -650,24 +852,6 @@ "type": "boolean", "default": true, "included": true - }, - { - "key": "github.copilot.chat.claudeAgent.enabled", - "name": "Claude3PIntegration", - "category": "InteractiveSession", - "minimumVersion": "1.113", - "localization": { - "description": { - "key": "github.copilot.chat.claudeAgent.enabled", - "value": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic Claude Agent SDK directly in the editor. Uses your existing Copilot subscription." - } - }, - "type": "boolean", - "default": true, - "included": true, - "referencedSettings": [ - "chat.agentHost.claudeAgent.enabled" - ] } ] } diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index 5e175afde2884..0aa037e696906 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -33,9 +33,30 @@ async function ensureNodeModules() { } async function getElectron() { + // `npm run electron` deletes and re-downloads `.build/electron` on every + // invocation. When preLaunch runs repeatedly (e.g. once per integration test + // section) this is both wasteful and a source of flaky failures on Windows, + // where the just-exited Electron process can still hold file locks while the + // directory is being removed and re-extracted. Skip the refresh when the + // already-present Electron matches the expected version; any detection + // failure falls back to a (re)download to preserve the previous behavior. + if (await isExpectedElectronInstalled()) { + return; + } await runProcess(npm, ['run', 'electron']); } +async function isExpectedElectronInstalled(): Promise { + try { + const { getElectronVersion } = await import('./util.ts'); + const { electronVersion } = getElectronVersion(); + const installedVersion = (await fs.readFile(path.join(rootDir, '.build', 'electron', 'version'), 'utf8')).trim().replace(/^v/, ''); + return installedVersion === electronVersion; + } catch { + return false; + } +} + async function ensureCompiled() { if (!(await exists('out'))) { await runProcess(npm, ['run', 'compile']); diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 967ff0108bf19..a51fee788a426 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -139,9 +139,9 @@ function transportCSS(module: string, enqueue: (module: string) => void, write: function _rewriteOrInlineUrls(contents: string, forceBase64: boolean): string { return _replaceURL(contents, (url) => { - const fontMatch = url.match(/^(.*).ttf\?(.*)$/); + const fontMatch = url.match(/^(.*)\.ttf(\?.*)?$/); if (fontMatch) { - const relativeFontPath = `${fontMatch[1]}.ttf`; // trim the query parameter + const relativeFontPath = `${fontMatch[1]}.ttf`; // trim the optional query parameter const fontPath = path.join(path.dirname(module), relativeFontPath); enqueue(fontPath); return relativeFontPath; diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index a443f51c0a136..930248b9da5e2 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -21,6 +21,8 @@ "--vscode-activityErrorBadge-foreground", "--vscode-activityWarningBadge-background", "--vscode-activityWarningBadge-foreground", + "--vscode-agentFeedbackEditorWidget-background", + "--vscode-agentFeedbackEditorWidget-border", "--vscode-agentFeedbackInputWidget-border", "--vscode-agentSessionReadIndicator-foreground", "--vscode-agentSessionSelectedBadge-border", @@ -1065,6 +1067,9 @@ "--chat-input-anim-angle", "--chat-input-anim-duration", "--chat-input-working-border-color1", + "--session-input-banner-anim-angle", + "--session-input-banner-anim-duration", + "--session-input-banner-working-border-color", "--chat-send-button-anim-angle", "--chat-setup-dialog-glow-angle", "--vscode-chat-font-family", diff --git a/build/lib/test/copilot.test.ts b/build/lib/test/copilot.test.ts new file mode 100644 index 0000000000000..b0f6b4f18a46a --- /dev/null +++ b/build/lib/test/copilot.test.ts @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { suite, test } from 'node:test'; +import { create } from 'tar'; +import { copilotPlatforms, ensureCopilotPlatformPackage, getCopilotExcludeFilter, getCopilotRuntimePrebuildFiles, prepareBuiltInCopilotRipgrepShim } from '../copilot.ts'; + +suite('copilot', () => { + test('keeps the public copilot platform package include list scoped to the selected package', () => { + const files = getCopilotRuntimePrebuildFiles('linux', 'x64'); + + assert.deepStrictEqual(files, [ + '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/clipboard/**', + '!node_modules/@github/copilot-linux-x64/foundry-local-sdk/**', + '!node_modules/@github/copilot-linux-x64/mxc-bin/**', + '!node_modules/@github/copilot-linux-x64/pvrecorder/**', + '!node_modules/@github/copilot-linux-x64/prebuilds/*/computer.node', + '!node_modules/@github/copilot-linux-x64/prebuilds/*/computer-use-mcp', + '!node_modules/@github/copilot-linux-x64/prebuilds/*/computer-use-mcp.exe', + '!node_modules/@github/copilot-linux-x64/prebuilds/*/Copilot Computer Use.app/**', + '!node_modules/@github/copilot-linux-x64/prebuilds/*/CopilotComputerUse.exe', + '!node_modules/@github/copilot-linux-x64/prebuilds/*/keytar.node', + '!node_modules/@github/copilot-linux-x64/prebuilds/*/cli-native.node', + ]); + assertCopilotPlatformPackageIncludes(files, 'node_modules/@github/copilot-linux-x64', [ + 'index.js', + 'app.js', + 'prebuilds/linux-x64/runtime.node', + 'prebuilds/linux-x64/pty.node', + ]); + assertCopilotStandaloneExecutableExcluded(files, 'node_modules/@github/copilot-linux-x64'); + assertOptionalCopilotNativeDependenciesExcluded(files, 'node_modules/@github/copilot-linux-x64'); + }); + + test('uses the linuxmusl package runtime for alpine builds', () => { + const files = getCopilotRuntimePrebuildFiles('alpine', 'x64'); + + assert.deepStrictEqual(files, [ + '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/clipboard/**', + '!node_modules/@github/copilot-linuxmusl-x64/foundry-local-sdk/**', + '!node_modules/@github/copilot-linuxmusl-x64/mxc-bin/**', + '!node_modules/@github/copilot-linuxmusl-x64/pvrecorder/**', + '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/computer.node', + '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/computer-use-mcp', + '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/computer-use-mcp.exe', + '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/Copilot Computer Use.app/**', + '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/CopilotComputerUse.exe', + '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/keytar.node', + '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/cli-native.node', + ]); + assertCopilotPlatformPackageIncludes(files, 'node_modules/@github/copilot-linuxmusl-x64', [ + 'index.js', + 'app.js', + 'prebuilds/linuxmusl-x64/runtime.node', + ]); + assertCopilotStandaloneExecutableExcluded(files, 'node_modules/@github/copilot-linuxmusl-x64'); + assertOptionalCopilotNativeDependenciesExcluded(files, 'node_modules/@github/copilot-linuxmusl-x64'); + }); + + test('uses the .exe package runtime for windows builds', () => { + assert.deepStrictEqual(getCopilotRuntimePrebuildFiles('win32', 'x64'), [ + '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/clipboard/**', + '!node_modules/@github/copilot-win32-x64/foundry-local-sdk/**', + '!node_modules/@github/copilot-win32-x64/mxc-bin/**', + '!node_modules/@github/copilot-win32-x64/pvrecorder/**', + '!node_modules/@github/copilot-win32-x64/prebuilds/*/computer.node', + '!node_modules/@github/copilot-win32-x64/prebuilds/*/computer-use-mcp', + '!node_modules/@github/copilot-win32-x64/prebuilds/*/computer-use-mcp.exe', + '!node_modules/@github/copilot-win32-x64/prebuilds/*/Copilot Computer Use.app/**', + '!node_modules/@github/copilot-win32-x64/prebuilds/*/CopilotComputerUse.exe', + '!node_modules/@github/copilot-win32-x64/prebuilds/*/keytar.node', + ]); + assertCopilotPlatformPackageIncludes(getCopilotRuntimePrebuildFiles('win32', 'x64'), 'node_modules/@github/copilot-win32-x64', [ + 'index.js', + 'app.js', + 'prebuilds/win32-x64/cli-native.node', + 'prebuilds/win32-x64/runtime.node', + 'prebuilds/win32-x64/conpty.node', + 'prebuilds/win32-x64/conpty_console_list.node', + 'prebuilds/win32-x64/conpty/OpenConsole.exe', + 'prebuilds/win32-x64/conpty/conpty.dll', + ]); + assertCopilotStandaloneExecutableExcluded(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/clipboard/**', + '!node_modules/@github/copilot-win32-arm64/foundry-local-sdk/**', + '!node_modules/@github/copilot-win32-arm64/mxc-bin/**', + '!node_modules/@github/copilot-win32-arm64/pvrecorder/**', + '!node_modules/@github/copilot-win32-arm64/prebuilds/*/computer.node', + '!node_modules/@github/copilot-win32-arm64/prebuilds/*/computer-use-mcp', + '!node_modules/@github/copilot-win32-arm64/prebuilds/*/computer-use-mcp.exe', + '!node_modules/@github/copilot-win32-arm64/prebuilds/*/Copilot Computer Use.app/**', + '!node_modules/@github/copilot-win32-arm64/prebuilds/*/CopilotComputerUse.exe', + '!node_modules/@github/copilot-win32-arm64/prebuilds/*/keytar.node', + ]); + assertOptionalCopilotNativeDependenciesExcluded(getCopilotRuntimePrebuildFiles('win32', 'x64'), 'node_modules/@github/copilot-win32-x64'); + assertCopilotStandaloneExecutableExcluded(getCopilotRuntimePrebuildFiles('win32', 'arm64'), 'node_modules/@github/copilot-win32-arm64'); + }); + + test('keeps macOS runtime prebuilds in the selected platform package', () => { + const files = getCopilotRuntimePrebuildFiles('darwin', 'arm64'); + + assertCopilotPlatformPackageIncludes(files, 'node_modules/@github/copilot-darwin-arm64', [ + 'index.js', + 'app.js', + 'sea-loader.js', + 'prebuilds/darwin-arm64/runtime.node', + 'prebuilds/darwin-arm64/pty.node', + 'prebuilds/darwin-arm64/spawn-helper', + ]); + assertCopilotStandaloneExecutableExcluded(files, 'node_modules/@github/copilot-darwin-arm64'); + assertOptionalCopilotNativeDependenciesExcluded(files, 'node_modules/@github/copilot-darwin-arm64'); + }); + + test('materializes missing target platform packages from the lockfile', () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-copilot-platform-test-')); + const nodeModulesRoot = path.join(repoRoot, 'node_modules'); + try { + fs.mkdirSync(nodeModulesRoot, { recursive: true }); + fs.writeFileSync(path.join(repoRoot, 'package-lock.json'), JSON.stringify({ + packages: { + 'node_modules/@github/copilot-darwin-x64': { + version: '1.0.64-1', + } + } + })); + + ensureCopilotPlatformPackage('darwin', 'x64', nodeModulesRoot, { + packPackage: (_packageName, _version, tempDir) => { + const packageRoot = path.join(tempDir, 'package'); + fs.mkdirSync(path.join(packageRoot, 'prebuilds', 'darwin-x64'), { recursive: true }); + fs.writeFileSync(path.join(packageRoot, 'index.js'), ''); + fs.writeFileSync(path.join(packageRoot, 'prebuilds', 'darwin-x64', 'runtime.node'), ''); + const tarball = path.join(tempDir, 'copilot-darwin-x64.tgz'); + create({ file: tarball, cwd: tempDir, gzip: true, sync: true }, ['package']); + return tarball; + } + }); + + assert(fs.existsSync(path.join(nodeModulesRoot, '@github', 'copilot-darwin-x64', 'index.js'))); + assert(fs.existsSync(path.join(nodeModulesRoot, '@github', 'copilot-darwin-x64', 'prebuilds', 'darwin-x64', 'runtime.node'))); + } finally { + fs.rmSync(repoRoot, { recursive: true, force: true }); + } + }); + + test('excludes standalone copilot executables from the platform package dependency stream', () => { + const files = getCopilotExcludeFilter('linux', 'x64'); + + assert(files.includes('**')); + assert(files.includes('!**/node_modules/@github/copilot-*/copilot')); + assert(files.includes('!**/node_modules/@github/copilot-*/copilot.exe')); + }); + + test('materializes target Copilot SDK prebuilds and tgrep for the built-in extension', () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-copilot-sdk-prebuild-test-')); + try { + const builtInCopilotExtensionDir = path.join(repoRoot, 'extensions', 'copilot'); + const extensionCopilotDir = path.join(builtInCopilotExtensionDir, 'node_modules', '@github', 'copilot'); + const appNodeModulesDir = path.join(repoRoot, 'node_modules'); + const platformPackageDir = path.join(appNodeModulesDir, '@github', 'copilot-win32-x64'); + + fs.mkdirSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'linux-x64'), { recursive: true }); + fs.writeFileSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'linux-x64', 'runtime.node'), ''); + fs.mkdirSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty'), { recursive: true }); + fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'runtime.node'), ''); + fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty.node'), ''); + fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe'), ''); + fs.mkdirSync(path.join(platformPackageDir, 'tgrep', 'bin', 'win32-x64'), { recursive: true }); + fs.writeFileSync(path.join(platformPackageDir, 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'), ''); + fs.mkdirSync(path.join(appNodeModulesDir, '@vscode', 'ripgrep-universal', 'bin', 'win32-x64'), { recursive: true }); + fs.writeFileSync(path.join(appNodeModulesDir, '@vscode', 'ripgrep-universal', 'bin', 'win32-x64', 'rg.exe'), ''); + + 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', 'conpty.node'))); + assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe'))); + assert(!fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'linux-x64'))); + assert(fs.existsSync(path.join(extensionCopilotDir, 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'))); + assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'))); + assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'ripgrep', 'bin', 'win32-x64', 'rg.exe'))); + } finally { + fs.rmSync(repoRoot, { recursive: true, force: true }); + } + }); + + test('strips all copilot platform packages for unsupported armhf builds', () => { + assert.deepStrictEqual( + getCopilotExcludeFilter('linux', 'armhf'), + [ + '**', + ...copilotPlatforms.map(platform => `!**/node_modules/@github/copilot-${platform}/**`), + '!**/node_modules/@github/copilot-*/copilot', + '!**/node_modules/@github/copilot-*/copilot.exe', + ] + ); + }); +}); + +function assertCopilotPlatformPackageIncludes(patterns: string[], packageDir: string, relativeFiles: string[]): void { + assert(patterns.includes(`${packageDir}/**`)); + for (const relativeFile of relativeFiles) { + assert(matchesGlob(`${packageDir}/${relativeFile}`, patterns), relativeFile); + } +} + +function assertCopilotStandaloneExecutableExcluded(patterns: string[], packageDir: string): void { + for (const executable of ['copilot', 'copilot.exe']) { + assert(patterns.includes(`!${packageDir}/${executable}`), executable); + assert(!matchesGlob(`${packageDir}/${executable}`, patterns), executable); + } +} + +function assertOptionalCopilotNativeDependenciesExcluded(patterns: string[], packageDir: string): void { + for (const dir of ['clipboard', 'foundry-local-sdk', 'mxc-bin', 'pvrecorder']) { + assert(patterns.includes(`!${packageDir}/${dir}/**`), dir); + assert(!matchesGlob(`${packageDir}/${dir}/index.js`, patterns), dir); + } + assert(patterns.includes(`!${packageDir}/prebuilds/*/computer.node`), 'computer.node'); + assert(!matchesGlob(`${packageDir}/prebuilds/linux-x64/computer.node`, patterns), 'computer.node'); + assert(patterns.includes(`!${packageDir}/prebuilds/*/computer-use-mcp`), 'computer-use-mcp'); + assert(!matchesGlob(`${packageDir}/prebuilds/darwin-arm64/computer-use-mcp`, patterns), 'computer-use-mcp'); + assert(patterns.includes(`!${packageDir}/prebuilds/*/computer-use-mcp.exe`), 'computer-use-mcp.exe'); + assert(!matchesGlob(`${packageDir}/prebuilds/win32-x64/computer-use-mcp.exe`, patterns), 'computer-use-mcp.exe'); + assert(patterns.includes(`!${packageDir}/prebuilds/*/Copilot Computer Use.app/**`), 'Copilot Computer Use.app'); + assert(!matchesGlob(`${packageDir}/prebuilds/darwin-arm64/Copilot Computer Use.app/Contents/MacOS/Copilot Computer Use`, patterns), 'Copilot Computer Use.app'); + assert(patterns.includes(`!${packageDir}/prebuilds/*/CopilotComputerUse.exe`), 'CopilotComputerUse.exe'); + assert(!matchesGlob(`${packageDir}/prebuilds/win32-x64/CopilotComputerUse.exe`, patterns), 'CopilotComputerUse.exe'); + assert(patterns.includes(`!${packageDir}/prebuilds/*/keytar.node`), 'keytar.node'); + assert(!matchesGlob(`${packageDir}/prebuilds/linux-x64/keytar.node`, patterns), 'keytar.node'); + + if (!packageDir.includes('win32')) { + assert(patterns.includes(`!${packageDir}/prebuilds/*/cli-native.node`), 'cli-native.node'); + assert(!matchesGlob(`${packageDir}/prebuilds/linux-x64/cli-native.node`, patterns), 'cli-native.node'); + } +} + +function matchesGlob(file: string, patterns: string[]): boolean { + let included = false; + for (const pattern of patterns) { + const isExclude = pattern.startsWith('!'); + const glob = isExclude ? pattern.slice(1) : pattern; + if (matchesPattern(file, glob)) { + included = !isExclude; + } + } + return included; +} + +function matchesPattern(file: string, pattern: string): boolean { + if (pattern.endsWith('/**') && !pattern.slice(0, -3).includes('*')) { + return file.startsWith(pattern.slice(0, -2)); + } + + if (pattern.includes('*')) { + const regex = new RegExp(`^${pattern.split('**').map(part => part.split('*').map(escapeRegExp).join('[^/]+')).join('.*')}$`); + return regex.test(file); + } + + return file === pattern; +} + +function escapeRegExp(value: string): string { + return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); +} diff --git a/build/lib/util.ts b/build/lib/util.ts index d463aec5e9999..760768dd07fc5 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -302,7 +302,7 @@ export function rimraf(dir: string): () => Promise { return c(); } - if (err.code === 'ENOTEMPTY' && ++retries < 5) { + if ((err.code === 'ENOTEMPTY' || err.code === 'EBUSY' || err.code === 'EPERM') && ++retries < 5) { return setTimeout(() => retry(), 10); } diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 7690e25d5fd31..d7bd81ccd104d 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -40,6 +40,7 @@ export const referenceGeneratedDepsByArch = { 'libdbus-1-3 (>= 1.9.14)', 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', + 'libglib2.0-0 (>= 2.12.0)', 'libglib2.0-0 (>= 2.39.4)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', @@ -47,7 +48,6 @@ export const referenceGeneratedDepsByArch = { 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 6)', 'libudev1 (>= 183)', 'libx11-6', 'libx11-6 (>= 2:1.4.99.1)', diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index eb1d73d011ac9..2fcb15e027192 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -22,7 +22,7 @@ import product from '../../product.json' with { type: 'json' }; // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.271:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index e9f414e4c7ef9..9c5241350ca59 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -109,15 +109,6 @@ export const referenceGeneratedDepsByArch = { 'libsmime3.so(NSS_3.10)(64bit)', 'libsmime3.so(NSS_3.2)(64bit)', 'libssl3.so(NSS_3.28)(64bit)', - 'libstdc++.so.6()(64bit)', - 'libstdc++.so.6(CXXABI_1.3)(64bit)', - 'libstdc++.so.6(CXXABI_1.3.8)(64bit)', - 'libstdc++.so.6(CXXABI_1.3.9)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.11)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.14)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', 'libudev.so.1()(64bit)', 'libudev.so.1(LIBUDEV_183)(64bit)', 'libutil.so.1()(64bit)', diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts index 0d00ac3926141..5df04a796373f 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -79,7 +79,7 @@ async function npmInstallAsync(dir: string, opts?: child_process.SpawnOptions): 'docker', 'run', '-e', 'GITHUB_TOKEN', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, - '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, + '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.gitconfig-distro:/root/.gitconfig`, '-v', `${process.env['VSCODE_NPMRC_PATH']}:/root/.npmrc`, '-w', path.resolve('/root/vscode', dir), process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], diff --git a/build/package-lock.json b/build/package-lock.json index 12162d6054f94..4271a5f06a58b 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -1320,28 +1320,28 @@ } }, "node_modules/@textlint/ast-node-types": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.2.tgz", - "integrity": "sha512-9ByYNzWV8tpz6BFaRzeRzIov8dkbSZu9q7IWqEIfmRuLWb2qbI/5gTvKcoWT1HYs4XM7IZ8TKSXcuPvMb6eorA==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.2.2.tgz", - "integrity": "sha512-oMVaMJ3exFvXhCj3AqmCbLaeYrTNLqaJnLJMIlmnRM3/kZdxvku4OYdaDzgtlI194cVxamOY5AbHBBVnY79kEg==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "15.2.2", - "@textlint/resolver": "15.2.2", - "@textlint/types": "15.2.2", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", "chalk": "^4.1.2", - "debug": "^4.4.1", - "js-yaml": "^3.14.1", - "lodash": "^4.17.21", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", @@ -1456,27 +1456,27 @@ } }, "node_modules/@textlint/module-interop": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.2.2.tgz", - "integrity": "sha512-2rmNcWrcqhuR84Iio1WRzlc4tEoOMHd6T7urjtKNNefpTt1owrTJ9WuOe60yD3FrTW0J/R0ux5wxUbP/eaeFOA==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.2.2.tgz", - "integrity": "sha512-4hGWjmHt0y+5NAkoYZ8FvEkj8Mez9TqfbTm3BPjoV32cIfEixl2poTOgapn1rfm73905GSO3P1jiWjmgvii13Q==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.2.2.tgz", - "integrity": "sha512-X2BHGAR3yXJsCAjwYEDBIk9qUDWcH4pW61ISfmtejau+tVqKtnbbvEZnMTb6mWgKU1BvTmftd5DmB1XVDUtY3g==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "15.2.2" + "@textlint/ast-node-types": "15.7.1" } }, "node_modules/@types/ansi-colors": { @@ -2408,21 +2408,11 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/argparse/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "BSD-3-Clause" + "license": "Python-2.0" }, "node_modules/arr-diff": { "version": "4.0.0", @@ -3403,20 +3393,6 @@ "node": ">=0.8.0" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/events": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", @@ -3642,17 +3618,17 @@ "dev": true }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -4004,9 +3980,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -4358,14 +4334,23 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -4509,10 +4494,20 @@ } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -4609,15 +4604,25 @@ } }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -4626,13 +4631,6 @@ "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/markdown-it/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -5488,26 +5486,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/rc-config-loader/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/rc-config-loader/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", diff --git a/build/rspack/package-lock.json b/build/rspack/package-lock.json index c125386c34cf6..29d6a8e71997c 100644 --- a/build/rspack/package-lock.json +++ b/build/rspack/package-lock.json @@ -794,9 +794,9 @@ ] }, "node_modules/@rspack/cli": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.7.11.tgz", - "integrity": "sha512-vUnflkq4F654wTEpCd+L4RYVbet8L2lNqLMmAGIZvoZddlXm4Duvg+eqcFE9iF8plAjFflRcU7DhB7WZa76pwg==", + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.7.12.tgz", + "integrity": "sha512-aiVj5hm12/rhkH/KTvs9BugpRuLEutIw5o3KmQDvSMd6EqEIASuCCUI3/97uaNP58hqZSl7mAotsLcwn/OCbJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2625,9 +2625,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2849,14 +2849,14 @@ "license": "MIT" }, "node_modules/launch-editor": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", - "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "node_modules/loader-runner": { @@ -4298,9 +4298,9 @@ } }, "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "dev": true, "license": "MIT", "engines": { @@ -4461,9 +4461,9 @@ } }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { diff --git a/build/setup-npm-registry.ts b/build/setup-npm-registry.ts index 670c3e339db0f..4560f2c000728 100644 --- a/build/setup-npm-registry.ts +++ b/build/setup-npm-registry.ts @@ -37,6 +37,10 @@ async function setup(url: string, file: string): Promise { * Main function to set up custom NPM registry */ async function main(url: string, dir?: string): Promise { + if (!url) { + throw new Error('Usage: node setup-npm-registry.ts [dir]. A registry URL is required.'); + } + const root = dir ?? process.cwd(); for await (const file of getPackageLockFiles(root)) { @@ -45,4 +49,7 @@ async function main(url: string, dir?: string): Promise { } } -main(process.argv[2], process.argv[3]); +main(process.argv[2], process.argv[3]).catch(err => { + console.error(err.message); + process.exit(1); +}); diff --git a/cgmanifest.json b/cgmanifest.json index b90a69f0349a5..afc549e769e0b 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "6b3fa66a923a9442c8ab0bc71b4b41ff24528d3b" + "commitHash": "21c21077f4937668031f80089a90618b7e1fe779" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "148.0.7778.97" + "version": "148.0.7778.271" }, { "component": { @@ -516,12 +516,12 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "848430679556aed0bd073f2bc263331ad84fa119", - "tag": "24.15.0" + "commitHash": "413e874773eef1e27a8397ddc949dbbe19cadb31", + "tag": "24.17.0" } }, "isOnlyProductionDependency": true, - "version": "24.15.0" + "version": "24.17.0" }, { "component": { @@ -529,13 +529,13 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "87740a867bddf434afec16e1f8b4f02235d3e7f7", - "tag": "42.2.0" + "commitHash": "d7bccf5b2f27969b4e7f34cf877f55cf0813be7a", + "tag": "42.5.0" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "42.2.0" + "version": "42.5.0" }, { "component": { diff --git a/eslint.config.js b/eslint.config.js index 51b085eebfe15..b9ba2cead680a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1577,6 +1577,7 @@ export default defineConfig( 'undici', 'undici-types', 'url', + 'module', 'util', 'vscode-regexpp', 'vscode-textmate', diff --git a/extensions/copilot/.esbuild.mts b/extensions/copilot/.esbuild.mts index 2377e0cc6e58b..e7654ab6879b6 100644 --- a/extensions/copilot/.esbuild.mts +++ b/extensions/copilot/.esbuild.mts @@ -246,6 +246,7 @@ const nodeSimulationWorkbenchUIBuildOptions = { // @ulugbekna: libs provided by node that need to be specified manually because of 'platform' is set to 'browser' 'fs', + 'os', 'path', 'readline', 'child_process', diff --git a/extensions/copilot/.github/CODEOWNERS b/extensions/copilot/.github/CODEOWNERS index 0e2c2176d7ee9..4bb631691afea 100644 --- a/extensions/copilot/.github/CODEOWNERS +++ b/extensions/copilot/.github/CODEOWNERS @@ -1,5 +1,5 @@ # Ensure Lad and Joao review cache relevant code -.github/workflows/pr-check-cache.yml @lszomoru @alexdima @joaomoreno -build/pr-check-cache-files.ts @lszomoru @alexdima @joaomoreno -test/base/cache-cli.ts @lszomoru @alexdima @joaomoreno -test/base/cache.ts @lszomoru @alexdima @joaomoreno +.github/workflows/pr-check-cache.yml @lszomoru @alexdima +build/pr-check-cache-files.ts @lszomoru @alexdima +test/base/cache-cli.ts @lszomoru @alexdima +test/base/cache.ts @lszomoru @alexdima diff --git a/extensions/copilot/.nvmrc b/extensions/copilot/.nvmrc index 5bf4400f22922..1dd37d53743d1 100644 --- a/extensions/copilot/.nvmrc +++ b/extensions/copilot/.nvmrc @@ -1 +1 @@ -24.15.0 +24.17.0 diff --git a/extensions/copilot/.vscodeignore b/extensions/copilot/.vscodeignore index 6ba2b99990919..e1fe34c33bcb7 100644 --- a/extensions/copilot/.vscodeignore +++ b/extensions/copilot/.vscodeignore @@ -22,7 +22,10 @@ assets/walkthroughs/** !node_modules/@vscode/copilot-typescript-server-plugin/dist/*.js node_modules/@github/copilot/index.js node_modules/@github/copilot/clipboard/** +node_modules/@github/copilot/foundry-local-sdk/** +node_modules/@github/copilot/mxc-bin/** node_modules/@github/copilot/prebuilds/** +node_modules/@github/copilot/pvrecorder/** node_modules/@github/copilot/sharp/** !node_modules/@github/copilot/package.json !node_modules/@github/copilot/sdk/**/package.json diff --git a/extensions/copilot/assets/debug-icon.svg b/extensions/copilot/assets/debug-icon.svg index 48ac425514ae8..fc3676b38e1a5 100644 --- a/extensions/copilot/assets/debug-icon.svg +++ b/extensions/copilot/assets/debug-icon.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/extensions/copilot/chat-lib/package-lock.json b/extensions/copilot/chat-lib/package-lock.json index 7480a9c237fcf..36c24bfb5a07d 100644 --- a/extensions/copilot/chat-lib/package-lock.json +++ b/extensions/copilot/chat-lib/package-lock.json @@ -147,25 +147,27 @@ } }, "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { - "version": "1.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", - "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0.tgz", + "integrity": "sha512-Y8rZOIMXQY/GwNRL+uLVuwIn9aEa/KnnggyYUmFxC1MigmRJCNH5NxMmxKSpddXF9SW6Z1ijRd6Pptd2A5OhGw==", + "license": "MIT", "dependencies": { - "@azure/core-tracing": "^1.0.0", + "@azure/core-tracing": "^1.2.0", "@azure/logger": "^1.0.0", - "@opentelemetry/api": "^1.4.1", - "@opentelemetry/core": "^1.15.2", - "@opentelemetry/instrumentation": "^0.41.2", - "tslib": "^2.2.0" + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "tslib": "^2.7.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/@opentelemetry/core": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", - "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -776,6 +778,18 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/core": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", @@ -801,18 +815,17 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", - "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", + "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", + "license": "Apache-2.0", "dependencies": { - "@types/shimmer": "^1.0.2", - "import-in-the-middle": "1.4.2", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.1", - "shimmer": "^1.2.1" + "@opentelemetry/api-logs": "0.211.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" @@ -869,6 +882,70 @@ "node": ">=14" } }, + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.8.0.tgz", + "integrity": "sha512-P3ZM8BGJ5mwjtyfAxRyxsCyWHvaj+xahdhLoS3YiPsEyTHcWTVzx2691C8SrGkpvro3tNFCsWuNNrvM+spKODg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz", @@ -1229,11 +1306,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/shimmer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.3.tgz", - "integrity": "sha512-F/IjUGnV6pIN7R4ZV4npHJVoNtaLZWvb+2/9gctxjb99wkpI7Ozg8VPogwDiTRyjLwZXAYxjvdg1KS8LTHKdDA==" - }, "node_modules/@vitest/expect": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", @@ -1372,9 +1444,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1383,11 +1455,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "deprecated": "package has been renamed to acorn-import-attributes", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "license": "MIT", "peerDependencies": { "acorn": "^8" @@ -1427,9 +1498,9 @@ } }, "node_modules/applicationinsights": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.7.tgz", - "integrity": "sha512-dxIVB2AAEMec3FDiYThgEbc9R4u1TatrzL+kFgOf+ABaEgHm8+i8ngVLHfKObjHvy2HHPf810OLWTrqyeWT/oA==", + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.8.tgz", + "integrity": "sha512-eB/EtAXJ6mDLLvHrtZj/7h31qUfnC2Npr2pHGqds5+1OP7BFLsn5us+HCkwTj7Q+1sHXujLphE5Cyvq5grtV6g==", "license": "MIT", "dependencies": { "@azure/core-auth": "1.7.2", @@ -1656,9 +1727,10 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" }, "node_modules/cls-hooked": { "version": "4.2.2", @@ -2381,6 +2453,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2627,6 +2700,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2661,14 +2735,15 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", - "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", + "license": "Apache-2.0", "dependencies": { - "acorn": "^8.8.2", - "acorn-import-assertions": "^1.9.0", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" } }, "node_modules/inflight": { @@ -2796,6 +2871,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -3511,9 +3587,10 @@ } }, "node_modules/module-details-from-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", - "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" }, "node_modules/monaco-editor": { "version": "0.44.0", @@ -3946,7 +4023,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "2.0.0", @@ -4161,22 +4239,23 @@ } }, "node_modules/require-in-the-middle": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", - "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.1" + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" }, "engines": { - "node": ">=8.6.0" + "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4318,18 +4397,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -4784,6 +4851,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -4990,9 +5058,9 @@ } }, "node_modules/undici": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", - "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" diff --git a/extensions/copilot/docs/monitoring/agent_monitoring.md b/extensions/copilot/docs/monitoring/agent_monitoring.md index 60e0422bbb3ab..76de7393f8aa4 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring.md @@ -36,7 +36,7 @@ Open **Settings** (`Ctrl+,`) and add: } ``` -> **Note:** You can also use environment variables instead of VS Code settings (see [Configuration](#configuration)). Environment variables always take precedence. +> **Note:** You can also use environment variables instead of VS Code settings (see [Configuration](#configuration)). Precedence is **enterprise policy > environment variables > settings**. ### 3. Generate Telemetry @@ -70,13 +70,17 @@ Open **Settings** (`Ctrl+,`) and search for `copilot otel`: | `github.copilot.chat.otel.exporterType` | string | `"otlp-http"` | `otlp-http`, `otlp-grpc`, `console`, or `file` | | `github.copilot.chat.otel.otlpEndpoint` | string | `"http://localhost:4318"` | OTLP collector endpoint | | `github.copilot.chat.otel.captureContent` | boolean | `false` | Capture full prompt/response content | +| `github.copilot.chat.otel.protocol` | string | `""` | OTLP wire protocol: `http/json` (default), `http/protobuf`, or `grpc` | +| `github.copilot.chat.otel.serviceName` | string | `""` | Override the `service.name` resource attribute | +| `github.copilot.chat.otel.resourceAttributes` | object | `{}` | Extra resource attributes (`{ "key": "value" }`) | +| `github.copilot.chat.otel.headers` | object | `{}` | Extra OTLP exporter headers, applied directly to the exporter (`{ "key": "value" }`) | | `github.copilot.chat.otel.maxAttributeSizeChars` | integer | `0` | Max characters per OTel content attribute (prompts, tool args/results, hook input/output). `0` (the default) disables truncation so backends with no per-attribute limit get full payloads. Set to a positive value to match your backend's per-attribute size limit — consult your backend's documentation. The value counts JavaScript string characters (UTF-16 code units); for non-ASCII content one character can be multiple UTF-8 bytes on the wire. | | `github.copilot.chat.otel.outfile` | string | `""` | File path for JSON-lines output | | `github.copilot.chat.otel.dbSpanExporter.enabled` | boolean | `false` | Persist OTel spans to a local SQLite database for the **Chat: Export Agent Traces DB** command. Implicitly enables OTel. | ### Environment Variables -Environment variables **always take precedence** over VS Code settings. +Environment variables take precedence over VS Code settings, and **enterprise managed settings (policy) take precedence over both** — admins can centrally mandate any `github.copilot.chat.otel.*` value. | Variable | Default | Description | |---|---|---| @@ -98,6 +102,7 @@ Environment variables **always take precedence** over VS Code settings. OTel is **off by default** with zero overhead. It activates when: +- enterprise policy enables it (managed `telemetry.enabled` or a managed endpoint), or - `COPILOT_OTEL_ENABLED=true`, or - `OTEL_EXPORTER_OTLP_ENDPOINT` is set, or - `github.copilot.chat.otel.enabled` is `true`, or @@ -471,7 +476,7 @@ All signals carry: | Attribute | Value | |---|---| -| `service.name` | `copilot-chat` (configurable via `OTEL_SERVICE_NAME`) | +| `service.name` | `copilot-chat` (override via the `github.copilot.chat.otel.serviceName` setting, `OTEL_SERVICE_NAME`, or enterprise policy) | | `service.version` | Extension version | | `session.id` | Unique per VS Code window | @@ -541,7 +546,7 @@ Content is captured in full with no truncation. } ``` -> **Note:** Authentication headers are only configurable via the `OTEL_EXPORTER_OTLP_HEADERS` environment variable (e.g., `Authorization=Bearer your-token`). See [Environment Variables](#environment-variables). +> **Note:** Authentication headers can be set via the `github.copilot.chat.otel.headers` setting (a `{ "key": "value" }` map applied directly to the exporter) or the `OTEL_EXPORTER_OTLP_HEADERS` environment variable, and can be mandated by enterprise policy. See [Environment Variables](#environment-variables). **File-based output (offline / CI):** diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_arch.md b/extensions/copilot/docs/monitoring/agent_monitoring_arch.md index 09c99b26de8ed..2eb3bc29fbac8 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring_arch.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring_arch.md @@ -209,10 +209,13 @@ Both export to the same OTLP endpoint. Bridge processor sits on Provider B, forw `resolveOTelConfig()` implements layered precedence: -1. `COPILOT_OTEL_*` env vars (highest) -2. `OTEL_EXPORTER_OTLP_*` standard env vars -3. VS Code settings (`github.copilot.chat.otel.*`) -4. Defaults (lowest) +1. Enterprise managed settings (policy) — highest; overrides env vars +2. `COPILOT_OTEL_*` env vars +3. `OTEL_EXPORTER_OTLP_*` standard env vars +4. VS Code settings (`github.copilot.chat.otel.*`) +5. Defaults (lowest) + +The OTLP wire protocol distinguishes `http/json` (default) from `http/protobuf`; `grpc` is selected by the gRPC exporter type. Kill switch: `telemetry.telemetryLevel === 'off'` → all OTel disabled. @@ -222,6 +225,7 @@ The resolved config records *how* OTel was enabled in `OTelConfig.enabledVia` (u | `enabledVia` | Trigger | |---|---| +| `policy` | Enterprise managed settings enable OTel (`telemetry.enabled` or a managed endpoint) | | `envVar` | `COPILOT_OTEL_ENABLED=true` | | `setting` | `github.copilot.chat.otel.enabled` is `true` | | `otlpEndpointEnvVar` | `OTEL_EXPORTER_OTLP_ENDPOINT` is set without an explicit enable | diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 765ab4aee4ee7..34fd4e9898566 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -1,31 +1,34 @@ { "name": "copilot-chat", - "version": "0.55.0", + "version": "0.56.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "copilot-chat", - "version": "0.55.0", + "version": "0.56.0", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@anthropic-ai/claude-agent-sdk": "0.2.112", "@anthropic-ai/sdk": "^0.82.0", "@github/blackbird-external-ingest-utils": "^0.3.0", - "@github/copilot": "^1.0.64-0", + "@github/copilot": "^1.0.67", "@google/genai": "^1.22.0", "@humanwhocodes/gitignore-to-minimatch": "1.0.2", "@microsoft/tiktokenizer": "^1.0.10", "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.212.0", - "@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", + "@opentelemetry/exporter-logs-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.219.0", "@opentelemetry/resources": "^2.5.1", "@opentelemetry/sdk-logs": "^0.212.0", "@opentelemetry/sdk-metrics": "^2.5.1", @@ -48,7 +51,7 @@ "isbinaryfile": "^5.0.4", "jsonc-parser": "^3.3.1", "lru-cache": "^11.1.0", - "markdown-it": "^14.1.1", + "markdown-it": "^14.2.0", "minimatch": "^10.2.1", "undici": "^7.24.1", "vscode-tas-client": "^0.1.84", @@ -94,16 +97,16 @@ "@vscode/lsif-language-service": "^0.1.0-pre.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", - "@vscode/test-web": "^0.0.80", + "@vscode/test-web": "^0.0.81", "@vscode/vsce": "3.6.0", "copyfiles": "^2.4.1", "csv-parse": "^6.0.0", "dotenv": "^17.2.0", - "electron": "^39.8.5", + "electron": "^42.5.0", "esbuild": "0.28.1", "fastq": "^1.19.1", "glob": "^11.1.0", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimist": "^1.2.8", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", @@ -116,7 +119,7 @@ "openai": "^6.7.0", "outdent": "^0.8.0", "picomatch": "^4.0.4", - "playwright": "^1.58.2", + "playwright": "^1.61.1", "prettier": "^3.6.2", "react": "^17.0.2", "react-dom": "17.0.2", @@ -141,7 +144,7 @@ "engines": { "node": ">=22.14.0", "npm": ">=9.0.0", - "vscode": "^1.127.0" + "vscode": "^1.128.0" } }, "node_modules/@anthropic-ai/claude-agent-sdk": { @@ -627,19 +630,36 @@ } }, "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { - "version": "1.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", - "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0.tgz", + "integrity": "sha512-Y8rZOIMXQY/GwNRL+uLVuwIn9aEa/KnnggyYUmFxC1MigmRJCNH5NxMmxKSpddXF9SW6Z1ijRd6Pptd2A5OhGw==", + "license": "MIT", "dependencies": { - "@azure/core-tracing": "^1.0.0", + "@azure/core-tracing": "^1.2.0", "@azure/logger": "^1.0.0", - "@opentelemetry/api": "^1.4.1", - "@opentelemetry/core": "^1.15.2", - "@opentelemetry/instrumentation": "^0.41.2", - "tslib": "^2.2.0" + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "tslib": "^2.7.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "node_modules/@babel/code-frame": { @@ -748,34 +768,35 @@ "node": ">=10" } }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz", + "integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", "progress": "^2.0.3", - "semver": "^6.2.0", + "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "engines": { - "node": ">=12" + "node": ">=22.12.0" }, "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "undici": "^7.24.4" } }, "node_modules/@emnapi/core": { @@ -2948,32 +2969,31 @@ "license": "MIT" }, "node_modules/@github/copilot": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.64-0.tgz", - "integrity": "sha512-PlH7ByBHjmPLqLXS4CE2q8hN6CFEfkCMV6ScBEzW/u73+KYQB4fGNouo8Lr8okL6D5CW5rzPJbsXyISyJqVOZg==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.67.tgz", + "integrity": "sha512-5YEY9LNXBT9Q8uShjCdYcornJJJhGtdIzSYla2+pjfXYpHsDVibqYubzYjfgffOUKFChyzOpH7n/868+t56iIg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "detect-libc": "^2.1.2", - "os-theme": "^0.0.8" + "detect-libc": "^2.1.2" }, "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.64-0", - "@github/copilot-darwin-x64": "1.0.64-0", - "@github/copilot-linux-arm64": "1.0.64-0", - "@github/copilot-linux-x64": "1.0.64-0", - "@github/copilot-linuxmusl-arm64": "1.0.64-0", - "@github/copilot-linuxmusl-x64": "1.0.64-0", - "@github/copilot-win32-arm64": "1.0.64-0", - "@github/copilot-win32-x64": "1.0.64-0" + "@github/copilot-darwin-arm64": "1.0.67", + "@github/copilot-darwin-x64": "1.0.67", + "@github/copilot-linux-arm64": "1.0.67", + "@github/copilot-linux-x64": "1.0.67", + "@github/copilot-linuxmusl-arm64": "1.0.67", + "@github/copilot-linuxmusl-x64": "1.0.67", + "@github/copilot-win32-arm64": "1.0.67", + "@github/copilot-win32-x64": "1.0.67" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.64-0.tgz", - "integrity": "sha512-97DUGiuYrkCYOlSSLWMmr+K0uGzAxz1JOL/GyO/7mNl6V/1xgs6Van1Jj+Dpj4ly96iHE8lUIW8cQNCG66644g==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.67.tgz", + "integrity": "sha512-CO3mpgFXcN6e7ZsSmjMkt1AKxMfb1+mjdn3yrf2DRnnWIURSK9kGvw+E+E1+YE37D1MBiUn/VOBmhRad5+vl0A==", "cpu": [ "arm64" ], @@ -2987,9 +3007,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.64-0.tgz", - "integrity": "sha512-2PXY4mSFtIjFdRaAt8PakegRgGtf6Sz9z6U/dIgVygNfctVNzaL5FH65PNPm8Y80jaDvEcz1/XY5MiQtxnlzZQ==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.67.tgz", + "integrity": "sha512-M20Hpn3bOJRkVwAIVRK4ZlX66AqtmGfXZRxZBRFQC045QIwcfmVUP45sTSgXDb4uHWeK0cZgdTdniHwKGtMplw==", "cpu": [ "x64" ], @@ -3003,9 +3023,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.64-0.tgz", - "integrity": "sha512-PLP+vR508fOTlCr9CSZiXi9geicHKXuX9jLGdwNqK2TMZO5TqCLz8wP7dBEmkdkeXcFKovMb8nQVB1Toc6xutw==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.67.tgz", + "integrity": "sha512-b4ePtFBow+Ior+aVLKA1hHxhR5wF+ql5CD7TSg/NHGYgc1kwD+3a9uKSENy05J5Lit/G/DZ9C6JwowvvdMWSKg==", "cpu": [ "arm64" ], @@ -3022,9 +3042,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.64-0.tgz", - "integrity": "sha512-NvVjQ69zr390ijzo2f75+v0DHm6xnvPbi67ugnKDk7ZPbx8P3vSxVdAnrzrrL4T3T8ng3pJANcC4p+eGbx+UDw==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.67.tgz", + "integrity": "sha512-4ynZyfKnWAdvEPAFDDBIz1wpFttcOTJu4Y8Mlz5oXCBA0NM/rwr8K4l7Adp8UzwbfmdrMJ9y+zivqRBMDbPInA==", "cpu": [ "x64" ], @@ -3041,9 +3061,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.64-0.tgz", - "integrity": "sha512-qCnVF5vIcTO74CukAENZo8e5nqXm4QUshuKN69aiZb5GOhVvyyIKsf5Jo7ikZt54jJBHycAMUKlTA8L3/nK+KA==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.67.tgz", + "integrity": "sha512-IjezxBU8fYUr/b5hEiniXqzwoOrJ4egrQSBbG96M+roLTqd9txP0MgxZtcRtKV7phRIdIGE109wwrn4H6hSqmA==", "cpu": [ "arm64" ], @@ -3060,9 +3080,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.64-0.tgz", - "integrity": "sha512-WDBEmkBk1RulTfdLK5IuttNBadjLOBpvQyonGQ/aLeaetRNNdapoygrSjFU7q1QBSenmCyanXH6D+TS7tP3Qsw==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.67.tgz", + "integrity": "sha512-Zy/rbja1lnhzDoNfn051H0EybCseCvjvH7WmbcHCayjXUjzXeKF6OmAt4hvqFZH87ttT3KbKtQ8/6oDUhhM2YQ==", "cpu": [ "x64" ], @@ -3079,9 +3099,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.64-0.tgz", - "integrity": "sha512-PC7yuUKcVbhli4bpzWFVT3juxj+v/iONazetNe3tMpHWza3W7MeFRifzAseSErKQCt2fHJth3m8bQAwFN2jfrA==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.67.tgz", + "integrity": "sha512-O3VFRS5v9NXRP8o+N1SvcFbBqECDzZP7XQBeBj2Vcrma80gdJc5GQub/w2mwmr1w5UbwgzJkRasm0Ec/jxbcoA==", "cpu": [ "arm64" ], @@ -3095,9 +3115,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.64-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.64-0.tgz", - "integrity": "sha512-d2fnUTIlqNxCqS2PuV+FD99ZOYBaX72OLtAmphbKyz36KyZ6D4ssiu8M4vHVTKWWdyc3TWiLsnIB+ryWdv1gGw==", + "version": "1.0.67", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.67.tgz", + "integrity": "sha512-td5tQ/nve5dB7RPvNglBZwa/6DJqiOBgacXXa1GpYcohqpCzoI8gONNkeaeyr6oF4iu5wXJ9krUNr6QXL4yB5Q==", "cpu": [ "x64" ], @@ -3693,16 +3713,16 @@ } }, "node_modules/@koa/router": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-15.4.0.tgz", - "integrity": "sha512-vKYlXtoCfcAN8z4dHiveYX55rTYOgHEYJNumK1WM9ZAwaArhreGVkyC1LTMGfUQUJyIO/SbwRFBOHeOCY8/MaQ==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-15.6.0.tgz", + "integrity": "sha512-iEOXlvGIBqSNkGXrg0XtMARAOm5zA24oedXxiTGEkrD4JgwVjfRDddCQvW1s4WEcwDYvyecRbf8BikXsuEEj8w==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", "http-errors": "^2.0.1", "koa-compose": "^4.1.0", - "path-to-regexp": "^8.3.0" + "path-to-regexp": "^8.4.2" }, "engines": { "node": ">= 20" @@ -4043,9 +4063,9 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.1.tgz", - "integrity": "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.8.0.tgz", + "integrity": "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4079,17 +4099,17 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.214.0.tgz", - "integrity": "sha512-SwmFRwO8mi6nndzbsjPgSFg7qy1WeNHRFD+s6uCsdiUDUt3+yzI2qiHE3/ub2f37+/CbeGcG+Ugc8Gwr6nu2Aw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-7SvzDCIclHWAcCwZ1MTOLcwn4BVNPGI3QxS/DJraPNe1TTL+4TvUBq5zeQV8tsnYvtDN7wKW2qocVmaCP2l7sQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/sdk-logs": "0.214.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/sdk-logs": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4099,9 +4119,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -4111,9 +4131,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4126,12 +4146,12 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4142,14 +4162,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", - "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4160,16 +4180,16 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.214.0.tgz", - "integrity": "sha512-9qv2Tl/Hq6qc5pJCbzFJnzA0uvlb9DgM70yGJPYf3bA5LlLkRCpcn81i4JbcIH4grlQIWY6A+W7YG0LLvS1BAw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.219.0.tgz", + "integrity": "sha512-mhl2HL6GmZI8b8PwPfqMws/5ovJfbRTxwc9Y5agVVHiQ+e5SL1btsFr/kJDgt7YCexDtsUn5HAreHQO9szFS0A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/sdk-logs": "0.214.0" + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/sdk-logs": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4179,9 +4199,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -4191,9 +4211,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4206,12 +4226,12 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4222,14 +4242,96 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", - "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.219.0.tgz", + "integrity": "sha512-Ayw4Gf71PS9jhBVaYywa4WsajnqfDehMkTdVH3TSAVHqPcsAv/AhH/wTNRYNt99szeYr6Gbd/D6RjZD77wAxHg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4239,20 +4341,37 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.214.0.tgz", - "integrity": "sha512-0NGxWHVYHgbp51SEzmsP+Hdups81eRs229STcSWHo3WO0aqY6RpJ9csxfyEtFgaNrBDv6UfOh0je4ss/ROS6XA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-metrics": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4262,9 +4381,9 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4277,12 +4396,12 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4293,16 +4412,16 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.214.0.tgz", - "integrity": "sha512-Tx/59RmjBgkXJ3qnsD04rpDrVWL53LU/czpgLJh+Ab98nAroe91I7vZ3uGN9mxwPS0jsZEnmqmHygVwB2vRMlA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.219.0.tgz", + "integrity": "sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-metrics": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4312,9 +4431,9 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4327,12 +4446,63 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.219.0.tgz", + "integrity": "sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4343,18 +4513,18 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.214.0.tgz", - "integrity": "sha512-FWRZ7AWoTryYhthralHkfXUuyO3l7cRsnr49WcDio1orl2a7KxT8aDZdwQtV1adzoUvZ9Gfo+IstElghCS4zfw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-trace-base": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4364,9 +4534,9 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4379,12 +4549,12 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4395,13 +4565,13 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", - "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4412,16 +4582,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.214.0.tgz", - "integrity": "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.219.0.tgz", + "integrity": "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-trace-base": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4431,9 +4601,9 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4446,12 +4616,12 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4462,13 +4632,80 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", - "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.219.0.tgz", + "integrity": "sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4479,31 +4716,42 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", - "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", + "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", + "license": "Apache-2.0", "dependencies": { - "@types/shimmer": "^1.0.2", - "import-in-the-middle": "1.4.2", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.1", - "shimmer": "^1.2.1" + "@opentelemetry/api-logs": "0.211.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.214.0.tgz", - "integrity": "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", + "integrity": "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-transformer": "0.214.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-transformer": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4513,9 +4761,9 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4528,15 +4776,15 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.214.0.tgz", - "integrity": "sha512-IDP6zcyA24RhNZ289MP6eToIZcinlmirHjX8v3zKCQ2ZhPpt5cGwkN91tCth337lqHIgWcTy90uKRiX/SzALDw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.219.0.tgz", + "integrity": "sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4546,9 +4794,9 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4561,18 +4809,17 @@ } }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.214.0.tgz", - "integrity": "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz", + "integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-logs": "0.214.0", - "@opentelemetry/sdk-metrics": "2.6.1", - "@opentelemetry/sdk-trace-base": "2.6.1", - "protobufjs": "^7.0.0" + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-metrics": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4582,9 +4829,9 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -4594,9 +4841,9 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4609,12 +4856,12 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4625,14 +4872,14 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", - "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4643,13 +4890,13 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", - "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4723,13 +4970,13 @@ } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", - "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", + "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4739,9 +4986,9 @@ } }, "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4754,12 +5001,12 @@ } }, "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4812,14 +5059,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.5.1.tgz", - "integrity": "sha512-9lopQ6ZoElETOEN0csgmtEV5/9C7BMfA7VtF4Jape3i954b6sTY2k3Xw3CxUTKreDck/vpAuJM+EDo4zheUw+A==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.8.0.tgz", + "integrity": "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-async-hooks": "2.5.1", - "@opentelemetry/core": "2.5.1", - "@opentelemetry/sdk-trace-base": "2.5.1" + "@opentelemetry/context-async-hooks": "2.8.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4829,9 +5076,9 @@ } }, "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", - "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4843,14 +5090,94 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.1.tgz", - "integrity": "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.8.0.tgz", + "integrity": "sha512-P3ZM8BGJ5mwjtyfAxRyxsCyWHvaj+xahdhLoS3YiPsEyTHcWTVzx2691C8SrGkpvro3tNFCsWuNNrvM+spKODg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.5.1", - "@opentelemetry/resources": "2.5.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4869,50 +5196,11 @@ "node": ">=14" } }, - "node_modules/@os-theme/darwin-arm64": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@os-theme/darwin-arm64/-/darwin-arm64-0.0.8.tgz", - "integrity": "sha512-gMsOs+8Ju396a5yyMWigkbA0dMTxD78U3HzG3mlpiAyn6hfd5dbyI4VGP+sfTB82KGgWLzIhWWTFX5UYY6iX0A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@os-theme/linux-x64": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@os-theme/linux-x64/-/linux-x64-0.0.8.tgz", - "integrity": "sha512-zvjmBUiSQPjM1RbhpsfCDYMJxW4eLlGmkFPnpteC/03X2lz6CjiX2hfbN2EWLxXjNnIje3Jqaen8IsqEnWrRBg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@os-theme/win32-x64": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@os-theme/win32-x64/-/win32-x64-0.0.8.tgz", - "integrity": "sha512-N3yxKNbVl2IBa/ncDuq55QhwqwUjnYLJxDKMEmYeJbLIV950qZNojPw3scXA6PbfxPZfIiRa8iz1pzNg9XxP8w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -4930,14 +5218,14 @@ } }, "node_modules/@playwright/browser-chromium": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.59.1.tgz", - "integrity": "sha512-XDwr0qOrzLXAuBAzg4WO/xctVMb+ldJ54yz9KCpFu8G8MVJzUVFO6BvK1tBtBl4DIoFcoFRKHgUGZT+8wOC8BQ==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.61.1.tgz", + "integrity": "sha512-t3/zE0i9gik5R/NpRs7G2Xo/6NPeABW6ReplGdtkeWeAkaV764CgFgoKjJo21D2xgjnvDvRYubqBUu4xl0VCqA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.59.1" + "playwright-core": "1.61.1" }, "engines": { "node": ">=18" @@ -4962,19 +5250,18 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -4983,12 +5270,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -5100,9 +5381,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5120,9 +5398,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5140,9 +5415,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5160,9 +5432,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5180,9 +5449,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5200,9 +5466,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5337,28 +5600,70 @@ } }, "node_modules/@secretlint/formatter": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.1.1.tgz", - "integrity": "sha512-Gpd8gTPN121SJ0h/9e6nWlZU7PitfhXUiEzW7Kyswg6kNGs+bSqmgTgWFtbo1VQ4ygJYiveWPNT05RCImBexJw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/resolver": "^10.1.1", - "@secretlint/types": "^10.1.1", - "@textlint/linter-formatter": "^14.8.4", - "@textlint/module-interop": "^14.8.4", - "@textlint/types": "^14.8.4", - "chalk": "^4.1.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", "debug": "^4.4.1", "pluralize": "^8.0.0", - "strip-ansi": "^6.0.1", + "strip-ansi": "^7.1.0", "table": "^6.9.0", - "terminal-link": "^2.1.1" + "terminal-link": "^4.0.0" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@secretlint/formatter/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/formatter/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@secretlint/node": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.1.1.tgz", @@ -5387,9 +5692,9 @@ "license": "MIT" }, "node_modules/@secretlint/resolver": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.1.1.tgz", - "integrity": "sha512-GdQzxnBtdBRjBULvZ8ERkaRqDp0njVwXrzBCav1pb0XshVk76C1cjeDqtTqM4RJ1Awo/g5U5MIWYztYv67v5Gg==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", "dev": true, "license": "MIT" }, @@ -5441,9 +5746,9 @@ } }, "node_modules/@secretlint/types": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.1.1.tgz", - "integrity": "sha512-/JGAvVkurVHkargk3AC7UxRy+Ymc+52AVBO/fZA5pShuLW2dX4O/rKc4n8cyhQiOb/3ym5ACSlLQuQ8apPfxrQ==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", "dev": true, "license": "MIT", "engines": { @@ -5466,18 +5771,6 @@ "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -5581,41 +5874,29 @@ "tslib": "^2.4.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@textlint/ast-node-types": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-14.8.4.tgz", - "integrity": "sha512-+fI7miec/r9VeniFV9ppL4jRCmHNsTxieulTUf/4tvGII3db5hGriKHC4p/diq1SkQ9Sgs7kg6UyydxZtpTz1Q==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-14.8.4.tgz", - "integrity": "sha512-sZ0UfYRDBNHnfMVBqLqqYnqTB7Ec169ljlmo+SEHR1T+dHUPYy1/DZK4p7QREXlBSFL4cnkswETCbc9xRodm4Q==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "14.8.4", - "@textlint/resolver": "14.8.4", - "@textlint/types": "14.8.4", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", "chalk": "^4.1.2", - "debug": "^4.4.1", - "js-yaml": "^3.14.1", - "lodash": "^4.17.21", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", @@ -5623,16 +5904,6 @@ "text-table": "^0.2.0" } }, - "node_modules/@textlint/linter-formatter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/@textlint/linter-formatter/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5640,20 +5911,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@textlint/linter-formatter/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@textlint/linter-formatter/node_modules/pluralize": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", @@ -5661,13 +5918,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@textlint/linter-formatter/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@textlint/linter-formatter/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5684,27 +5934,27 @@ } }, "node_modules/@textlint/module-interop": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-14.8.4.tgz", - "integrity": "sha512-1LdPYLAVpa27NOt6EqvuFO99s4XLB0c19Hw9xKSG6xQ1K82nUEyuWhzTQKb3KJ5Qx7qj14JlXZLfnEuL6A16Bw==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-14.8.4.tgz", - "integrity": "sha512-nMDOgDAVwNU9ommh+Db0U+MCMNDPbQ/1HBNjbnHwxZkCpcT6hsAJwBe38CW/DtWVUv8yeR4R40IYNPT84srNwA==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-14.8.4.tgz", - "integrity": "sha512-9nyY8vVXlr8hHKxa6+37omJhXWCwovMQcgMteuldYd4dOxGm14AK2nXdkgtKEUQnzLGaXy46xwLCfhQy7V7/YA==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "14.8.4" + "@textlint/ast-node-types": "15.7.1" } }, "node_modules/@tybys/wasm-util": { @@ -5729,18 +5979,6 @@ "@types/node": "*" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -5835,12 +6073,6 @@ "@types/unist": "*" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", - "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==", - "dev": true - }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -5868,15 +6100,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -5978,15 +6201,6 @@ "@types/react": "*" } }, - "node_modules/@types/responselike": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz", - "integrity": "sha512-TiGnitEDxj2X0j+98Eqk5lv/Cij8oHd32bU4D/Yw6AOq7vvTk0gSD2GPj0G/HkvhMoVsdlhYF4yqqlyPBTM6Sg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/sarif": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", @@ -6021,11 +6235,6 @@ "@types/node": "*" } }, - "node_modules/@types/shimmer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.3.tgz", - "integrity": "sha512-F/IjUGnV6pIN7R4ZV4npHJVoNtaLZWvb+2/9gctxjb99wkpI7Ozg8VPogwDiTRyjLwZXAYxjvdg1KS8LTHKdDA==" - }, "node_modules/@types/sinon": { "version": "17.0.4", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", @@ -6120,16 +6329,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz", - "integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.36.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", @@ -6950,26 +7149,26 @@ } }, "node_modules/@vscode/test-web": { - "version": "0.0.80", - "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.80.tgz", - "integrity": "sha512-QgsRPp8IuPcdbUdVtqQkFN/sGd+6cr6g0ENEVFOHwJbQqZtJSiZD5w0e6tgmoeq9nBjNqZCq87OO1GkwFHkPyw==", + "version": "0.0.81", + "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.81.tgz", + "integrity": "sha512-qAYNX1mf4hE0L3T/186J8AH+Z7Inm81OHMACkkyKE2J6HJZlXou0OgABkSvd8gt0BiPjI+V+xkduAaQ8Kcjexg==", "dev": true, "license": "MIT", "dependencies": { "@koa/cors": "^5.0.0", - "@koa/router": "^15.3.0", - "@playwright/browser-chromium": "^1.58.2", + "@koa/router": "^15.6.0", + "@playwright/browser-chromium": "^1.61.0", "gunzip-maybe": "^1.4.2", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "koa": "^3.1.2", + "http-proxy-agent": "^9.1.0", + "https-proxy-agent": "^9.1.0", + "koa": "^3.2.1", "koa-morgan": "^1.0.1", "koa-mount": "^4.2.0", "koa-static": "^5.0.0", "minimist": "^1.2.8", - "playwright": "^1.58.2", - "tar-fs": "^3.1.1", - "tinyglobby": "^0.2.15", + "playwright": "^1.61.0", + "tar-fs": "^3.1.2", + "tinyglobby": "^0.2.17", "vscode-uri": "^3.1.0" }, "bin": { @@ -6979,10 +7178,50 @@ "node": ">=20" } }, + "node_modules/@vscode/test-web/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vscode/test-web/node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vscode/test-web/node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@vscode/test-web/node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6995,13 +7234,14 @@ } }, "node_modules/@vscode/test-web/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -7265,10 +7505,11 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", "peerDependencies": { "acorn": "^8" } @@ -7324,6 +7565,22 @@ } } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -7374,9 +7631,9 @@ } }, "node_modules/applicationinsights": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.7.tgz", - "integrity": "sha512-dxIVB2AAEMec3FDiYThgEbc9R4u1TatrzL+kFgOf+ABaEgHm8+i8ngVLHfKObjHvy2HHPf810OLWTrqyeWT/oA==", + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.8.tgz", + "integrity": "sha512-eB/EtAXJ6mDLLvHrtZj/7h31qUfnC2Npr2pHGqds5+1OP7BFLsn5us+HCkwTj7Q+1sHXujLphE5Cyvq5grtV6g==", "license": "MIT", "dependencies": { "@azure/core-auth": "1.7.2", @@ -7577,11 +7834,19 @@ } }, "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, "node_modules/balanced-match": { "version": "1.0.2", @@ -7590,20 +7855,26 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", - "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "dev": true, "license": "Apache-2.0", - "optional": true + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } }, "node_modules/bare-fs": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", - "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -7624,42 +7895,45 @@ } }, "node_modules/bare-os": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", - "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.2.tgz", + "integrity": "sha512-h530JsrkYi8518ZfR57GHaLoI5YzXkGGEV0Y+mf4KYPBn4OnNajiznwkDq7FgE+Vnmyss9Utnzi44y7sowiAXA==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } }, "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "streamx": "^2.21.0" + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" }, "peerDependencies": { + "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, "bare-buffer": { "optional": true }, @@ -7669,12 +7943,11 @@ } }, "node_modules/bare-url": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", - "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-path": "^3.0.0" } @@ -7828,13 +8101,6 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "dev": true, - "optional": true - }, "node_modules/boundary": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", @@ -8030,43 +8296,6 @@ "node": ">=8" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -8228,9 +8457,10 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" }, "node_modules/cli-cursor": { "version": "5.0.0", @@ -8308,18 +8538,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cls-hooked": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", @@ -8777,6 +8995,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -8792,6 +9011,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, + "optional": true, "engines": { "node": ">=10" }, @@ -8844,15 +9064,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -8946,13 +9157,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "optional": true - }, "node_modules/diagnostic-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", @@ -9123,24 +9327,41 @@ "license": "MIT" }, "node_modules/electron": { - "version": "39.8.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.5.tgz", - "integrity": "sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==", + "version": "42.5.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-42.5.0.tgz", + "integrity": "sha512-cYEKS9XFz+c9fAB4jI0x49yz1FFzB55r3q96wu9YkwwJMv7t9202IE/ltlgy6yitl/J4M7C8JQcmUqdzDvPl/w==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" }, "bin": { - "electron": "cli.js" + "electron": "cli.js", + "install-electron": "install.js" }, "engines": { - "node": ">= 12.20.55" + "node": ">= 22.12.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" } }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/embla-carousel": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", @@ -9226,12 +9447,29 @@ } }, "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/error-ex": { @@ -9383,13 +9621,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "optional": true - }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -9501,20 +9732,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -9544,6 +9761,16 @@ "node": ">= 0.6" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -9714,26 +9941,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9801,15 +10008,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -9920,17 +10118,17 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -9962,20 +10160,6 @@ "dev": true, "optional": true }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -10136,21 +10320,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -10210,24 +10379,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -10304,31 +10455,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -10452,9 +10578,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -10582,12 +10708,6 @@ "node": ">= 0.6" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -10620,19 +10740,6 @@ "node": ">= 14" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -10683,14 +10790,15 @@ "dev": true }, "node_modules/import-in-the-middle": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", - "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", + "license": "Apache-2.0", "dependencies": { - "acorn": "^8.8.2", - "acorn-import-assertions": "^1.9.0", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" } }, "node_modules/inflight": { @@ -10862,6 +10970,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -11409,10 +11518,20 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -11430,13 +11549,6 @@ "bignumber.js": "^9.0.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -11478,28 +11590,12 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "optional": true - }, "node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -11622,9 +11718,9 @@ } }, "node_modules/koa": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-3.1.2.tgz", - "integrity": "sha512-2LOQnFKu3m0VxpE+5sb5+BRTSKrXmNxGgxVRiKwD9s5KQB1zID/FRXhtzeV7RT1L2GVpdEEAfVuclFOMGl1ikA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.2.1.tgz", + "integrity": "sha512-e7IpWJrnanNUroVK2taAgMxoEZvHLXdQiNjeExSu/DEIWm83jaKGBgb7tLmu2rMYpA027qFB3iLR/k3AVpFRnA==", "dev": true, "license": "MIT", "dependencies": { @@ -11769,16 +11865,20 @@ } }, "node_modules/koa/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/leven": { @@ -11942,9 +12042,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11966,9 +12063,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11990,9 +12084,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12014,9 +12105,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12083,9 +12171,20 @@ } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" } @@ -12236,15 +12335,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/lru-cache": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", @@ -12293,14 +12383,24 @@ } }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -12309,19 +12409,6 @@ "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12461,15 +12548,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -12820,9 +12898,10 @@ } }, "node_modules/module-details-from-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", - "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" }, "node_modules/monaco-editor": { "version": "0.44.0", @@ -13098,18 +13177,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -13595,20 +13662,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/os-theme": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/os-theme/-/os-theme-0.0.8.tgz", - "integrity": "sha512-u1q3bLSv5uMHNIiPItkfDrHXu6ZFs2juwqxWREFM/uVBa+7Kkhy2v49LmJev2JcinGwqiEccElB/XsH9gwasuA==", - "license": "MIT", - "optionalDependencies": { - "@os-theme/darwin-arm64": "0.0.8", - "@os-theme/linux-x64": "0.0.8", - "@os-theme/win32-x64": "0.0.8" - }, - "peerDependencies": { - "typescript": "^5" - } - }, "node_modules/outdent": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", @@ -13633,15 +13686,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -13812,7 +13856,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "2.0.0", @@ -13887,7 +13932,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -13928,13 +13974,13 @@ } }, "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.2" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -13947,9 +13993,9 @@ } }, "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -13974,19 +14020,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/playwright/node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -14118,6 +14151,7 @@ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -14156,24 +14190,23 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", - "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -14192,6 +14225,24 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -14268,18 +14319,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -14574,22 +14613,23 @@ } }, "node_modules/require-in-the-middle": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", - "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.1" + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" }, "engines": { - "node": ">=8.6.0" + "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -14602,12 +14642,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, "node_modules/resolve-path": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", @@ -14672,18 +14706,6 @@ "node": ">= 0.6" } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -14731,24 +14753,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/rolldown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", @@ -15051,13 +15055,6 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "optional": true - }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -15118,35 +15115,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -15461,6 +15429,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -15522,13 +15508,6 @@ "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "optional": true - }, "node_modules/stack-chain": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", @@ -15592,17 +15571,15 @@ "license": "MIT" }, "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", "dev": true, "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -15829,6 +15806,7 @@ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" }, @@ -15849,9 +15827,9 @@ } }, "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", "dependencies": { @@ -15859,13 +15837,17 @@ "supports-color": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -15903,24 +15885,6 @@ "dev": true, "license": "MIT" }, - "node_modules/table/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/table/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -16091,56 +16055,37 @@ "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.2.33.tgz", "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "streamx": "^2.12.5" } }, - "node_modules/terminal-link/node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16151,7 +16096,8 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/textextensions": { "version": "6.11.0", @@ -16529,9 +16475,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", - "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -16556,15 +16502,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -17183,9 +17120,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -17344,13 +17281,16 @@ } }, "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "dev": true, + "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/yazl": { diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 0e917130e913e..3eae6bd612003 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -1,8 +1,8 @@ { "name": "copilot-chat", - "displayName": "GitHub Copilot Chat", + "displayName": "GitHub Copilot", "description": "AI chat features powered by Copilot", - "version": "0.55.0", + "version": "0.56.0", "build": "1", "completionsCoreVersion": "1.378.1799", "internalLargeStorageAriaKey": "ec712b3202c5462fb6877acae7f1f9d7-c19ad55e-3e3c-4f99-984b-827f6d95bd9e-6917", @@ -22,7 +22,7 @@ "icon": "assets/copilot.png", "pricing": "Trial", "engines": { - "vscode": "^1.127.0", + "vscode": "^1.128.0", "npm": ">=9.0.0", "node": ">=22.14.0" }, @@ -1826,6 +1826,11 @@ "secret": true, "description": "API key for OpenAI", "title": "API Key" + }, + "zeroDataRetentionEnabled": { + "type": "boolean", + "default": false, + "markdownDescription": "Whether Zero Data Retention (ZDR) is enabled for this provider group. When `true`, OpenAI Responses requests from this group do not send `previous_response_id`." } }, "required": [ @@ -1835,7 +1840,10 @@ }, { "vendor": "ollama", - "displayName": "Ollama", + "displayName": "Ollama (Deprecated)", + "deprecation": { + "link": "vscode:extension/Ollama.ollama" + }, "configuration": { "type": "object", "properties": { @@ -2136,6 +2144,30 @@ "additionalProperties": { "type": "string" } + }, + "modelOptions": { + "type": "object", + "markdownDescription": "Sampling parameters to send with requests to this model. These override Copilot's defaults but are overridden by explicit per-request values. Set a property to `null` to omit it and use the model server's default.", + "properties": { + "temperature": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "markdownDescription": "Sampling temperature. Set to `null` to omit the parameter." + }, + "top_p": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "maximum": 1, + "markdownDescription": "Nucleus sampling probability. Set to `null` to omit the parameter." + } + }, + "additionalProperties": false } }, "required": [ @@ -3447,6 +3479,11 @@ "tags": [ "onExp" ] + }, + "github.copilot.chat.imageUpload.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "%github.copilot.config.imageUpload.enabled%" } } }, @@ -3592,15 +3629,6 @@ "onExp" ] }, - "github.copilot.chat.imageUpload.enabled": { - "type": "boolean", - "default": true, - "tags": [ - "experimental", - "onExp" - ], - "markdownDescription": "%github.copilot.config.imageUpload.enabled%" - }, "github.copilot.chat.codeGeneration.instructions": { "markdownDeprecationMessage": "%github.copilot.config.codeGeneration.instructions.deprecated%", "type": "array", @@ -4013,7 +4041,7 @@ }, "github.copilot.chat.gemini35FlashReducedToolUsePrompt.enabled": { "type": "boolean", - "default": false, + "default": true, "tags": [ "experimental", "onExp" @@ -4035,19 +4063,6 @@ "clear-both" ] }, - "github.copilot.chat.responsesApiReasoningSummary": { - "type": "string", - "default": "detailed", - "markdownDescription": "%github.copilot.config.responsesApiReasoningSummary%", - "tags": [ - "experimental", - "onExp" - ], - "enum": [ - "off", - "detailed" - ] - }, "github.copilot.chat.responsesApiContextManagement.enabled": { "type": "boolean", "default": false, @@ -4066,10 +4081,10 @@ "onExp" ] }, - "github.copilot.chat.responsesApi.persistentCoT.enabled": { + "github.copilot.chat.responsesApi.promptCacheBreakpoint.enabled": { "type": "boolean", "default": false, - "markdownDescription": "%github.copilot.config.responsesApi.persistentCoT.enabled%", + "markdownDescription": "%github.copilot.config.responsesApi.promptCacheBreakpoint.enabled%", "tags": [ "experimental", "onExp" @@ -4104,7 +4119,7 @@ }, "github.copilot.chat.gemini3GetChangedFilesTool.enabled": { "type": "boolean", - "default": true, + "default": false, "markdownDescription": "%github.copilot.config.gemini3GetChangedFilesTool.enabled%", "tags": [ "experimental", @@ -4327,13 +4342,34 @@ }, "github.copilot.chat.tools.grepSearch.outputFormat": { "type": "string", - "default": "tag", - "enum": ["grep", "tag"], + "default": "grep", + "enum": [ + "grep", + "tag" + ], "markdownDescription": "%github.copilot.chat.tools.grepSearch.outputFormat%", "tags": [ "experimental", "onExp" ] + }, + "github.copilot.chat.tools.grepSearch.defaultMaxResults": { + "type": "number", + "default": 20, + "markdownDescription": "%github.copilot.chat.tools.grepSearch.defaultMaxResults%", + "tags": [ + "experimental", + "onExp" + ] + }, + "github.copilot.chat.tools.grepSearch.maxResultsCap": { + "type": "number", + "default": 200, + "markdownDescription": "%github.copilot.chat.tools.grepSearch.maxResultsCap%", + "tags": [ + "experimental", + "onExp" + ] } } }, @@ -5273,7 +5309,10 @@ "type": "boolean", "default": false, "scope": "application", - "markdownDescription": "Enable OpenTelemetry trace/metric/log emission for Copilot Chat operations. Configurable in user settings only. Env var `COPILOT_OTEL_ENABLED` takes precedence. Requires window reload.", + "policyReference": { + "name": "CopilotOtelEnabled" + }, + "markdownDescription": "Enable OpenTelemetry trace/metric/log emission for Copilot Chat operations. Precedence: enterprise policy > env var `COPILOT_OTEL_ENABLED` > user setting. Requires window reload.", "tags": [ "advanced" ] @@ -5288,7 +5327,28 @@ ], "default": "otlp-http", "scope": "application", - "markdownDescription": "OTel exporter type for Copilot Chat telemetry. Configurable in user settings only. Requires window reload.", + "policyReference": { + "name": "CopilotOtelProtocol" + }, + "markdownDescription": "OTel exporter type for Copilot Chat telemetry. Configurable in user settings or managed by enterprise policy (policy takes precedence). Requires window reload.", + "tags": [ + "advanced" + ] + }, + "github.copilot.chat.otel.protocol": { + "type": "string", + "enum": [ + "", + "http/json", + "http/protobuf", + "grpc" + ], + "default": "", + "scope": "application", + "policyReference": { + "name": "CopilotOtelOtlpProtocol" + }, + "markdownDescription": "OTLP wire protocol for Copilot Chat OTel data, mirroring `OTEL_EXPORTER_OTLP_PROTOCOL`. `http/protobuf` selects the protobuf-over-HTTP exporter; the default (empty) uses `http/json`. Precedence: enterprise policy > env var > user setting. Requires window reload.", "tags": [ "advanced" ] @@ -5297,7 +5357,10 @@ "type": "string", "default": "http://localhost:4318", "scope": "application", - "markdownDescription": "OTLP collector endpoint URL for Copilot Chat OTel data. Configurable in user settings only. Env var `OTEL_EXPORTER_OTLP_ENDPOINT` takes precedence. Requires window reload.", + "policyReference": { + "name": "CopilotOtelEndpoint" + }, + "markdownDescription": "OTLP collector endpoint URL for Copilot Chat OTel data. Precedence: enterprise policy > env var `OTEL_EXPORTER_OTLP_ENDPOINT` > user setting. Requires window reload.", "tags": [ "advanced" ] @@ -5306,7 +5369,52 @@ "type": "boolean", "default": false, "scope": "application", - "markdownDescription": "Capture input/output messages, system instructions, and tool definitions in OTel telemetry. **Contains potentially sensitive data.** Configurable in user settings only. Env var `COPILOT_OTEL_CAPTURE_CONTENT` takes precedence. Requires window reload.", + "policyReference": { + "name": "CopilotOtelCaptureContent" + }, + "markdownDescription": "Capture input/output messages, system instructions, and tool definitions in OTel telemetry. **Contains potentially sensitive data.** Precedence: enterprise policy > env var `COPILOT_OTEL_CAPTURE_CONTENT` > user setting. Requires window reload.", + "tags": [ + "advanced" + ] + }, + "github.copilot.chat.otel.serviceName": { + "type": "string", + "default": "", + "scope": "application", + "policyReference": { + "name": "CopilotOtelServiceName" + }, + "markdownDescription": "OTel `service.name` resource attribute for Copilot Chat OTel data. Configurable in user settings only. Env var `OTEL_SERVICE_NAME` takes precedence over the setting; enterprise policy takes precedence over both. Requires window reload.", + "tags": [ + "advanced" + ] + }, + "github.copilot.chat.otel.resourceAttributes": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": {}, + "scope": "application", + "policyReference": { + "name": "CopilotOtelResourceAttributes" + }, + "markdownDescription": "Additional OTel resource attributes for Copilot Chat OTel data, as a `{ \"key\": \"value\" }` map. Configurable in user settings only. Merged per-key with `OTEL_RESOURCE_ATTRIBUTES` env (env wins over the setting); enterprise policy wins over both. Requires window reload.", + "tags": [ + "advanced" + ] + }, + "github.copilot.chat.otel.headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": {}, + "scope": "application", + "policyReference": { + "name": "CopilotOtelHeaders" + }, + "markdownDescription": "Extra OTLP exporter headers (e.g. auth tokens) for Copilot Chat OTel data, as a `{ \"key\": \"value\" }` map. Applied directly to the OTLP exporter, not via environment variables. Configurable in user settings only. Merged per-key with `OTEL_EXPORTER_OTLP_HEADERS` env (env wins over the setting); enterprise policy wins over both. **Contains potentially sensitive credentials.** Requires window reload.", "tags": [ "advanced" ] @@ -5325,7 +5433,10 @@ "type": "string", "default": "", "scope": "application", - "markdownDescription": "File path for file-based OTel exporter output (JSON-lines). When set, overrides exporter type to `file`. Configurable in user settings only. Requires window reload.", + "policyReference": { + "name": "CopilotOtelOutfile" + }, + "markdownDescription": "File path for file-based OTel exporter output (JSON-lines). When set, overrides exporter type to `file`. Configurable in user settings or managed by enterprise policy (policy takes precedence). Requires window reload.", "tags": [ "advanced" ] @@ -5480,69 +5591,69 @@ "agents/changes/actions/primary": [ { "command": "github.copilot.sessions.initializeRepository", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == workspace && !sessions.hasGitRepository && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.isolationMode == workspace && !sessions.hasGitRepository && !sessions.isAgentHostSession", "group": "0_init@1" }, { "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && !sessions.isMergeBaseBranchProtected && !sessions.hasPullRequest && (sessions.hasUncommittedChanges || sessions.hasOutgoingChanges) && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && !sessions.isMergeBaseBranchProtected && !sessions.hasPullRequest && (sessions.hasUncommittedChanges || sessions.hasOutgoingChanges) && !sessions.isAgentHostSession", "group": "1_merge@1" }, { "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && !sessions.isMergeBaseBranchProtected && !sessions.hasPullRequest && (sessions.hasUncommittedChanges || sessions.hasOutgoingChanges) && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && !sessions.isMergeBaseBranchProtected && !sessions.hasPullRequest && (sessions.hasUncommittedChanges || sessions.hasOutgoingChanges) && !sessions.isAgentHostSession", "group": "1_merge@2" }, { "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && !sessions.hasPullRequest && sessions.hasBranchChanges && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && !sessions.hasPullRequest && sessions.hasBranchChanges && !sessions.isAgentHostSession", "group": "2_pull_request@1" }, { "command": "github.copilot.chat.createDraftPullRequestCopilotCLIAgentSession.createDraftPR", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && !sessions.hasPullRequest && sessions.hasBranchChanges && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && !sessions.hasPullRequest && sessions.hasBranchChanges && !sessions.isAgentHostSession", "group": "2_pull_request@2" }, { "command": "github.copilot.sessions.commit", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges && !sessions.isAgentHostSession", "group": "3_commit@1" }, { "command": "github.copilot.sessions.commitAndSync", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges && !sessions.isAgentHostSession", "group": "3_commit@2" }, { "command": "github.copilot.sessions.sync", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && sessions.hasUpstream && !sessions.hasUncommittedChanges && (sessions.hasIncomingChanges || sessions.hasOutgoingChanges) && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && sessions.hasUpstream && !sessions.hasUncommittedChanges && (sessions.hasIncomingChanges || sessions.hasOutgoingChanges) && !sessions.isAgentHostSession", "group": "4_sync@1" }, { "command": "github.copilot.claude.sessions.initializeRepository", - "when": "chatSessionType == claude-code && isSessionsWindow && !sessions.hasGitRepository", + "when": "sessionType == claude-code && isSessionsWindow && !sessions.hasGitRepository", "group": "init@1" }, { "command": "github.copilot.claude.sessions.commit", - "when": "chatSessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges", + "when": "sessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges", "group": "commit@1" }, { "command": "github.copilot.claude.sessions.commitAndSync", - "when": "chatSessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges && sessions.hasUpstream", + "when": "sessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges && sessions.hasUpstream", "group": "commit@2" }, { "command": "github.copilot.claude.sessions.sync", - "when": "chatSessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && !sessions.hasUncommittedChanges && sessions.hasUpstream", + "when": "sessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && !sessions.hasUncommittedChanges && sessions.hasUpstream", "group": "sync@1" } ], "agents/change/inline": [ { "command": "github.copilot.sessions.discardChanges", - "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && !isActiveSessionArchived && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && sessions.hasGitRepository && !sessionIsArchived && !sessions.isAgentHostSession", "group": "navigation@2" } ], @@ -6292,12 +6403,12 @@ "chatSessions/item/context": [ { "command": "github.copilot.claude.sessions.rename", - "when": "chatSessionType == claude-code && chatSessionProviderId == default-copilot", + "when": "sessionType == claude-code && sessionProviderId == default-copilot", "group": "1_edit@4" }, { "command": "github.copilot.cli.sessions.rename", - "when": "chatSessionType == copilotcli && chatSessionProviderId == default-copilot", + "when": "sessionType == copilotcli && sessionProviderId == default-copilot", "group": "1_edit@4" } ], @@ -6311,7 +6422,7 @@ "chat/input/editing/sessionTitleToolbar": [ { "command": "github.copilot.sessions.refreshChanges", - "when": "chatSessionType == copilotcli && isSessionsWindow && !sessions.isAgentHostSession", + "when": "sessionType == copilotcli && isSessionsWindow && !sessions.isAgentHostSession", "group": "9_refresh@1" } ], @@ -6681,6 +6792,7 @@ "canDelegate": true, "requiresCustomModels": true, "supportsAutoModel": false, + "requiresCopilotSignIn": true, "capabilities": { "supportsFileAttachments": true, "supportsImageAttachments": true @@ -6741,6 +6853,7 @@ "description": "%github.copilot.session.providerDescription.background%", "when": "config.github.copilot.chat.backgroundAgent.enabled", "supportsAutoModel": true, + "requiresCopilotSignIn": true, "capabilities": { "supportsFileAttachments": true, "supportsProblemAttachments": true, @@ -6799,6 +6912,7 @@ "description": "%github.copilot.session.providerDescription.cloud%", "when": "config.github.copilot.chat.cloudAgent.enabled", "supportsAutoModel": false, + "requiresCopilotSignIn": true, "capabilities": { "supportsFileAttachments": true }, @@ -7038,16 +7152,16 @@ "@vscode/lsif-language-service": "^0.1.0-pre.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", - "@vscode/test-web": "^0.0.80", + "@vscode/test-web": "^0.0.81", "@vscode/vsce": "3.6.0", "copyfiles": "^2.4.1", "csv-parse": "^6.0.0", "dotenv": "^17.2.0", - "electron": "^39.8.5", + "electron": "^42.5.0", "esbuild": "0.28.1", "fastq": "^1.19.1", "glob": "^11.1.0", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimist": "^1.2.8", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", @@ -7060,7 +7174,7 @@ "openai": "^6.7.0", "outdent": "^0.8.0", "picomatch": "^4.0.4", - "playwright": "^1.58.2", + "playwright": "^1.61.1", "prettier": "^3.6.2", "react": "^17.0.2", "react-dom": "17.0.2", @@ -7086,19 +7200,22 @@ "@anthropic-ai/claude-agent-sdk": "0.2.112", "@anthropic-ai/sdk": "^0.82.0", "@github/blackbird-external-ingest-utils": "^0.3.0", - "@github/copilot": "^1.0.64-0", + "@github/copilot": "^1.0.67", "@google/genai": "^1.22.0", "@humanwhocodes/gitignore-to-minimatch": "1.0.2", "@microsoft/tiktokenizer": "^1.0.10", "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.212.0", - "@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", + "@opentelemetry/exporter-logs-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.219.0", "@opentelemetry/resources": "^2.5.1", "@opentelemetry/sdk-logs": "^0.212.0", "@opentelemetry/sdk-metrics": "^2.5.1", @@ -7121,7 +7238,7 @@ "isbinaryfile": "^5.0.4", "jsonc-parser": "^3.3.1", "lru-cache": "^11.1.0", - "markdown-it": "^14.1.1", + "markdown-it": "^14.2.0", "minimatch": "^10.2.1", "undici": "^7.24.1", "vscode-tas-client": "^0.1.84", @@ -7129,6 +7246,7 @@ }, "overrides": { "string_decoder": "npm:string_decoder@1.2.0", + "yauzl": "^3.3.1", "zod": "3.25.76" }, "vscodeCommit": "94c8e2adc50e26ef70af85a0de3a9efed757acaa", diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index 61c63fcca21b9..aecb9d3a3396f 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -278,7 +278,9 @@ "github.copilot.config.autoFix": "Automatically fix diagnostics for edited files.", "github.copilot.config.rateLimitAutoSwitchToAuto": "Automatically switch to the Auto model and retry when you hit a per-model rate limit.", "github.copilot.tools.createNewWorkspace.userDescription": "Scaffold a new workspace in VS Code", - "github.copilot.chat.tools.grepSearch.outputFormat": "The output format for the grep search tool. Can be either 'grep' or 'tag'. The default is 'tag'.", + "github.copilot.chat.tools.grepSearch.outputFormat": "The output format for the grep search tool. Can be either 'grep' or 'tag'. The default is 'grep'.", + "github.copilot.chat.tools.grepSearch.defaultMaxResults": "The default maximum number of results to return from the grep search tool. The default is 20.", + "github.copilot.chat.tools.grepSearch.maxResultsCap": "The maximum number of results that can be returned from the grep search tool. The default is 200.", "copilot.tools.errors.description": "Check errors for a particular file", "copilot.tools.applyPatch.description": "Edit text files in the workspace", "copilot.tools.findTestFiles.description": "For a source code file, find the file that contains the tests. For a test file, find the file that contains the code under test", @@ -344,10 +346,9 @@ "github.copilot.config.anthropic.promptCaching.extendedTtlMessages": "Also extend the 1 hour prompt cache TTL to message-level breakpoints. Requires `chat.anthropic.promptCaching.extendedTtl` to be enabled; has no effect on its own.\n\n**Note**: This is an experimental feature.", "github.copilot.config.modelCapabilityOverrides": "Per-model capability overrides keyed by model id, intended for evaluating preview and tenanted models against an existing model's capability profile. For each model id, declare an aliased `family`. Setting `family` to a known production family (e.g. `\"claude-opus-4.7\"`) makes the model receive that family's full capability profile — Anthropic family detection, latest Opus prompt, multi-replace tools, tool search, context editing, extended cache TTL — without a code change.\n\n**Note**: This is an advanced setting for evaluation use; it is not intended for regular end-user configuration.", "github.copilot.config.useResponsesApi": "Use the Responses API instead of the Chat Completions API when supported. Enables reasoning and reasoning summaries.\n\n**Note**: This is an experimental feature that is not yet activated for all users.\n\n**Important**: For Custom Endpoint models, the API type is independent of this setting and is determined per-model via the `apiType` property, or inferred from the `url` path when omitted.", - "github.copilot.config.responsesApiReasoningSummary": "Sets the reasoning summary style used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.", "github.copilot.config.responsesApiContextManagement.enabled": "Enables context management for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.", "github.copilot.config.responsesApi.promptCacheKey.enabled": "Enables prompt cache key being set for the Responses API.", - "github.copilot.config.responsesApi.persistentCoT.enabled": "Enables persistent chain of thought for supported Responses API models.", + "github.copilot.config.responsesApi.promptCacheBreakpoint.enabled": "Enables explicit prompt cache breakpoint markers for the Responses API.", "github.copilot.config.updated53CodexPrompt.enabled": "Enables the updated prompt for gpt-5.3-codex model.", "github.copilot.config.claude47OpusPrompt.enabled": "Enables the updated system prompt tuned for the Claude Opus 4.7 model.", "github.copilot.config.gpt55GetChangedFilesTool.enabled": "Enables the Get Changed Files tool for gpt-5.5 models.", @@ -416,7 +417,7 @@ "github.copilot.config.claudeAgent.useSdkExtension": "Load the Claude Agent SDK from the `ms-vscode.vscode-claude-sdk` extension (installed on demand) instead of the version bundled with Copilot Chat.", "github.copilot.config.claudeAgent.sdkExtensionInstallTimeout": "Maximum time in milliseconds to wait for the `ms-vscode.vscode-claude-sdk` extension to be installed and detected when loading the Claude Agent SDK from the marketplace.", "github.copilot.config.cli.mcp.enabled": "Enable Model Context Protocol (MCP) server for Copilot CLI.", - "github.copilot.config.cli.sandbox.enabled": "Run Copilot CLI tools (such as the terminal) inside a sandbox to limit what they can access on your system.", + "github.copilot.config.cli.sandbox.enabled": "Run Copilot CLI tools (such as the terminal) inside a sandbox to limit what they can access on your system. The sandbox only applies to requests that run with default approvals — it is not used when bypassing approvals — and is not supported on Windows yet.", "github.copilot.config.cli.sandbox.enabled.offDescription": "Disable sandboxing for Copilot CLI tools.", "github.copilot.config.cli.sandbox.enabled.onDescription": "Enable sandboxing for Copilot CLI tools.", "github.copilot.config.cli.sandbox.enabled.allowNetworkDescription": "Enable sandboxing for Copilot CLI tools and allow all network domains.", diff --git a/extensions/copilot/script/alternativeAction/index.ts b/extensions/copilot/script/alternativeAction/index.ts index 8f90cd8fa273d..93bc883633bba 100644 --- a/extensions/copilot/script/alternativeAction/index.ts +++ b/extensions/copilot/script/alternativeAction/index.ts @@ -46,7 +46,7 @@ async function extractFromCsv(csvContents: string): Promise<(Scoring.t | undefin if (!altAction || !altAction.recording) { return undefined; } - return Processor.createScoringForAlternativeAction(altAction, coalesce([parseSuggestedEdit(obj.postProcessingOutcome.suggestedEdit)]), false); + return Processor.createScoring(altAction.recording.entries ?? [], altAction.recording.requestTime, coalesce([parseSuggestedEdit(obj.postProcessingOutcome.suggestedEdit)]), false); }); return scoredEdits; @@ -113,7 +113,12 @@ async function handleAlternativeActionJson(inputFilePath: string) { } isAccepted = data.suggestionStatus === 'accepted'; } - const scoring = Processor.createScoringForAlternativeAction(altAction, edits, isAccepted); + const altActionRecording = altAction.recording; + if (!altActionRecording) { + console.error('Alternative action has no recording'); + return; + } + const scoring = Processor.createScoring(altActionRecording.entries ?? [], altActionRecording.requestTime, edits, isAccepted); if (!scoring) { console.error('Failed to create scoring from alternative action'); return; diff --git a/extensions/copilot/script/electron/simulationWorkbench.css b/extensions/copilot/script/electron/simulationWorkbench.css index 5fc0b94d0c03f..7cab1b6f52978 100644 --- a/extensions/copilot/script/electron/simulationWorkbench.css +++ b/extensions/copilot/script/electron/simulationWorkbench.css @@ -193,6 +193,11 @@ box-shadow: 0 0 0 2pt #0000004b; } +/* Only the focused adhoc editor should show a text caret. */ +.adhoc-request-editor .monaco-editor:not(.focused) .cursors-layer .cursor { + display: none !important; +} + .file-editor-draggable-border { height: 5px; width: 95%; diff --git a/extensions/copilot/script/electron/simulationWorkbenchMain.js b/extensions/copilot/script/electron/simulationWorkbenchMain.js index f462ee04aa8c1..87f6a97a59a62 100644 --- a/extensions/copilot/script/electron/simulationWorkbenchMain.js +++ b/extensions/copilot/script/electron/simulationWorkbenchMain.js @@ -33,6 +33,27 @@ function createWindow() { }); } +/** + * Installs a standard application menu. This is required for the clipboard + * keyboard shortcuts (Cmd/Ctrl+C/V/X) to be delivered to the web contents - + * without an Edit menu containing the paste role, pasting into editors (e.g. + * the Monaco editors in the "Adhoc request sender" mode) does not work, + * especially on macOS. + */ +function setupApplicationMenu() { + /** @type {Electron.MenuItemConstructorOptions[]} */ + const template = []; + if (process.platform === 'darwin') { + template.push({ role: 'appMenu' }); + } + template.push({ role: 'fileMenu' }); + template.push({ role: 'editMenu' }); + template.push({ role: 'viewMenu' }); + template.push({ role: 'windowMenu' }); + + electron.Menu.setApplicationMenu(electron.Menu.buildFromTemplate(template)); +} + app.on('ready', () => { if (process.argv.includes('--help')) { console.log(`Options: @@ -40,6 +61,7 @@ app.on('ready', () => { --grep=STRING Pre-populates simulation workbench 'grep' input box.`); app.quit(); } + setupApplicationMenu(); registerListeners(); createWindow(); }); diff --git a/extensions/copilot/script/postinstall.ts b/extensions/copilot/script/postinstall.ts index a3a322b2c5932..46a3533bb18c5 100644 --- a/extensions/copilot/script/postinstall.ts +++ b/extensions/copilot/script/postinstall.ts @@ -60,54 +60,144 @@ const treeSitterGrammars: ITreeSitterGrammar[] = [ ]; const REPO_ROOT = path.join(__dirname, '..'); +const COPILOT_PACKAGE_DIR = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot'); +const COPILOT_CLI_TOP_LEVEL_DIRS = [ + 'worker', + 'definitions', + 'builtin-skills', + 'builtin', + 'tgrep', + 'queries', + 'prebuilds', + 'ripgrep', + 'foundry-local-sdk', + 'pvrecorder', + 'mxc-bin', + 'clipboard', + 'copilot-sdk', + 'schemas', + 'preloads', +]; + +interface ICopilotPackageJson { + exports?: Record; +} + +function isLinuxMuslRuntime(): boolean { + if (process.platform !== 'linux') { + return false; + } + + const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined; + return !report?.header?.glibcVersionRuntime; +} + +function getCopilotPlatformPackageCandidates(): string[] { + const arch = process.arch; + + if (process.platform === 'linux') { + const linuxCandidates = [`linux-${arch}`, `linuxmusl-${arch}`]; + return isLinuxMuslRuntime() ? linuxCandidates.reverse() : linuxCandidates; + } + + return [`${process.platform}-${arch}`]; +} + +async function resolveCopilotCliSourceDir(): Promise { + const tried: string[] = []; + for (const platformPackage of getCopilotPlatformPackageCandidates()) { + const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', `copilot-${platformPackage}`); + tried.push(sourceDir); + if (fs.existsSync(path.join(sourceDir, 'sdk', 'index.js'))) { + return sourceDir; + } + } + + if (fs.existsSync(path.join(COPILOT_PACKAGE_DIR, 'sdk', 'index.js'))) { + return COPILOT_PACKAGE_DIR; + } + + throw new Error(`Could not find @github/copilot SDK files. Tried: ${[COPILOT_PACKAGE_DIR, ...tried].join(', ')}`); +} + +async function ensureCopilotSdkExport() { + const packageJsonPath = path.join(COPILOT_PACKAGE_DIR, 'package.json'); + const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8')) as ICopilotPackageJson; + packageJson.exports = { + ...(packageJson.exports ?? {}), + './sdk': { + types: './sdk/index.d.ts', + import: './sdk/index.js' + } + }; + + await fs.promises.writeFile(packageJsonPath, `${JSON.stringify(packageJson, undefined, 2)}\n`); +} + +async function materializeCopilotCliSdkLayout(): Promise { + const sourceDir = await resolveCopilotCliSourceDir(); + + if (sourceDir !== COPILOT_PACKAGE_DIR) { + await copyCopilotCLIFolders(path.join(sourceDir, 'sdk'), path.join(COPILOT_PACKAGE_DIR, 'sdk')); + for (const dir of COPILOT_CLI_TOP_LEVEL_DIRS) { + const sourcePath = path.join(sourceDir, dir); + if (fs.existsSync(sourcePath)) { + await copyCopilotCLIFolders(sourcePath, path.join(COPILOT_PACKAGE_DIR, dir)); + } + } + + for (const entry of await fs.promises.readdir(sourceDir)) { + if (entry.startsWith('tree-sitter') && entry.endsWith('.wasm')) { + await fs.promises.copyFile(path.join(sourceDir, entry), path.join(COPILOT_PACKAGE_DIR, entry)); + } + } + } + + await ensureCopilotSdkExport(); + return sourceDir; +} async function removeCopilotCLIShim() { - const shimsPath = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'shims.txt'); + const shimsPath = path.join(COPILOT_PACKAGE_DIR, 'shims.txt'); await fs.promises.rm(shimsPath, { force: true }).catch(() => { /* ignore */ }); } -/** - * @github/copilot/sdk/index.js depends on @github/copilot/worker/*.js files. - * We need to copy these files into the sdk directory to ensure they are available at runtime. - */ -async function copyCopilotCliWorkerFiles() { - const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'worker'); - const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'worker'); - - await copyCopilotCLIFolders(sourceDir, targetDir); +async function removeCopilotCliWorkerFiles() { + const targetDir = path.join(COPILOT_PACKAGE_DIR, 'sdk', 'worker'); + await fs.promises.rm(targetDir, { recursive: true, force: true }); } -async function copyCopilotCliTGrepFiles() { - const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'tgrep'); - const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'tgrep'); +async function copyCopilotCliTGrepFiles(copilotCliSourceDir: string) { + const sourceDir = path.join(copilotCliSourceDir, 'tgrep'); + const targetDir = path.join(COPILOT_PACKAGE_DIR, 'sdk', 'tgrep'); await copyCopilotCLIFolders(sourceDir, targetDir); } -async function copyCopilotCliDefinitionFiles() { - const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'definitions'); - const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'definitions'); +async function copyCopilotCliDefinitionFiles(copilotCliSourceDir: string) { + const sourceDir = path.join(copilotCliSourceDir, 'definitions'); + const targetDir = path.join(COPILOT_PACKAGE_DIR, 'sdk', 'definitions'); await copyCopilotCLIFolders(sourceDir, targetDir); } -async function copyCopilotCliSkillsFiles() { - const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'builtin-skills'); - const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'builtin-skills'); +async function copyCopilotCliSkillsFiles(copilotCliSourceDir: string) { + const sourceDir = path.join(copilotCliSourceDir, 'builtin-skills'); + const targetDir = path.join(COPILOT_PACKAGE_DIR, 'sdk', 'builtin-skills'); await copyCopilotCLIFolders(sourceDir, targetDir); } -async function copyCopilotCliQueryFiles() { - const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'queries'); - const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'queries'); +async function copyCopilotCliQueryFiles(copilotCliSourceDir: string) { + const sourceDir = path.join(copilotCliSourceDir, 'queries'); + const targetDir = path.join(COPILOT_PACKAGE_DIR, 'sdk', 'queries'); await copyCopilotCLIFolders(sourceDir, targetDir); } -async function copyCopilotCliPrebuildFiles() { - const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'prebuilds'); - const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'prebuilds'); +async function copyCopilotCliPrebuildFiles(copilotCliSourceDir: string) { + const sourceDir = path.join(copilotCliSourceDir, 'prebuilds'); + const targetDir = path.join(COPILOT_PACKAGE_DIR, 'sdk', 'prebuilds'); await fs.promises.rm(targetDir, { recursive: true, force: true }); await fs.promises.mkdir(targetDir, { recursive: true }); await fs.promises.cp(sourceDir, targetDir, { @@ -115,9 +205,6 @@ async function copyCopilotCliPrebuildFiles() { try { if (fs.statSync(src).isFile()) { const normalizedSrc = src.split(path.sep).join(path.posix.sep); - if (normalizedSrc.includes('/prebuilds/linuxmusl-')) { - return false; - } return src.endsWith('computer.node') || src.endsWith('runtime.node') || src.endsWith('cli-native.node') @@ -191,13 +278,14 @@ async function main() { 'node_modules/@github/blackbird-external-ingest-utils/pkg/nodejs/external_ingest_utils_bg.wasm', ], 'dist'); + const copilotCliSourceDir = await materializeCopilotCliSdkLayout(); await removeCopilotCLIShim(); - await copyCopilotCliWorkerFiles(); - await copyCopilotCliDefinitionFiles(); - await copyCopilotCliSkillsFiles(); - await copyCopilotCliTGrepFiles(); - await copyCopilotCliQueryFiles(); - await copyCopilotCliPrebuildFiles(); + await removeCopilotCliWorkerFiles(); + await copyCopilotCliDefinitionFiles(copilotCliSourceDir); + await copyCopilotCliSkillsFiles(copilotCliSourceDir); + await copyCopilotCliTGrepFiles(copilotCliSourceDir); + await copyCopilotCliQueryFiles(copilotCliSourceDir); + await copyCopilotCliPrebuildFiles(copilotCliSourceDir); // Check if the base cache file exists (dev-only sanity check, non-fatal in CI) const baseCachePath = path.join('test', 'simulation', 'cache', 'base.sqlite'); diff --git a/extensions/copilot/script/setup/getEnv.mts b/extensions/copilot/script/setup/getEnv.mts index 0cd82c9a7371b..dd04e6e7d2c3f 100644 --- a/extensions/copilot/script/setup/getEnv.mts +++ b/extensions/copilot/script/setup/getEnv.mts @@ -57,8 +57,7 @@ async function fetchSecrets(): Promise<{ [key: string]: string | undefined }> { secrets['HMAC_SECRET'] = await fetchSecret(keyVaultClient, 'hmac-secret'); if (!process.stdin.isTTY) { // only in automation - secrets['GITHUB_OAUTH_TOKEN'] = await fetchSecret(keyVaultClient, 'capi-oauth'); - secrets['VSCODE_COPILOT_CHAT_TOKEN'] = await fetchSecret(keyVaultClient, 'copilot-token'); + secrets['GITHUB_OAUTH_TOKEN'] = await fetchSecret(keyVaultClient, 'capi-oauth-pipeline-token'); secrets['BLACKBIRD_EMBEDDINGS_KEY'] = await fetchSecret(keyVaultClient, 'vsc-aoai-key'); secrets['BLACKBIRD_REDIS_CACHE_KEY'] = await fetchSecret(keyVaultClient, 'blackbird-redis-cache-key'); diff --git a/extensions/copilot/src/extension/byok/common/byokProvider.ts b/extensions/copilot/src/extension/byok/common/byokProvider.ts index a966f2e63c18d..5427c6904d764 100644 --- a/extensions/copilot/src/extension/byok/common/byokProvider.ts +++ b/extensions/copilot/src/extension/byok/common/byokProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { Disposable, LanguageModelChatInformation, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart } from 'vscode'; import { CopilotToken } from '../../../platform/authentication/common/copilotToken'; -import { EndpointEditToolName, IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; +import { EndpointEditToolName, IChatModelInformation, IChatModelRequestOptions, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { TokenizerType } from '../../../util/common/tokenizer'; export const enum BYOKAuthType { @@ -56,6 +56,7 @@ export interface BYOKModelCapabilities { streaming?: boolean; editTools?: EndpointEditToolName[]; requestHeaders?: Record; + modelOptions?: IChatModelRequestOptions; supportedEndpoints?: ModelSupportedEndpoint[]; zeroDataRetentionEnabled?: boolean; supportsReasoningEffort?: string[]; @@ -128,6 +129,7 @@ export function resolveModelInfo(modelId: string, providerName: string, knownMod model_picker_enabled: true, supported_endpoints: knownModelInfo?.supportedEndpoints, zeroDataRetentionEnabled: knownModelInfo?.zeroDataRetentionEnabled, + modelOptions: knownModelInfo?.modelOptions, reasoningEffortFormat: knownModelInfo?.reasoningEffortFormat }; if (knownModelInfo?.requestHeaders && Object.keys(knownModelInfo.requestHeaders).length > 0) { diff --git a/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts b/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts index 8a78e8e0e3f7e..caaaf0dbfad25 100644 --- a/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts +++ b/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts @@ -71,6 +71,21 @@ describe('resolveModelInfo', () => { expect(info.capabilities.supports.reasoning_effort).toBeUndefined(); expect(info.reasoningEffortFormat).toBeUndefined(); }); + + it('propagates configured model options into chat-endpoint inputs', () => { + const info = resolveModelInfo('m1', 'TestProvider', undefined, { + ...baseCapabilities, + modelOptions: { + temperature: null, + top_p: 0.95, + }, + }); + + expect(info.modelOptions).toEqual({ + temperature: null, + top_p: 0.95, + }); + }); }); describe('isClientBYOKAllowed', () => { diff --git a/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts b/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts index 0cf838e3c235a..aed5d1b64dd52 100644 --- a/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts +++ b/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts @@ -265,10 +265,10 @@ export class OpenAIEndpoint extends ChatEndpoint { body.previous_response_id = undefined; } this._applyReasoningEffort(body, options); - return body; + return this._applyConfiguredModelOptions(body, options); } else if (this.useMessagesApi) { // Delegate to base ChatEndpoint for Messages API dispatch - return super.createRequestBody(options); + return this._applyConfiguredModelOptions(super.createRequestBody(options), options); } else { // Handle Chat Completions: provide callback for thinking data processing const supportsThinking = !!this.modelMetadata.capabilities.supports.thinking; @@ -290,8 +290,32 @@ export class OpenAIEndpoint extends ChatEndpoint { }; const body = createCapiRequestBody(options, this.model, callback); this._applyReasoningEffort(body, options); + return this._applyConfiguredModelOptions(body, options); + } + } + + private _applyConfiguredModelOptions(body: IEndpointBody, options: ICreateEndpointBodyOptions): IEndpointBody { + const modelOptions = this.modelMetadata.modelOptions; + if (!modelOptions) { return body; } + + for (const key of ['temperature', 'top_p'] as const) { + const requestValue = options.requestOptions?.[key]; + if (requestValue !== undefined) { + body[key] = requestValue; + continue; + } + + const configuredValue = modelOptions[key]; + if (configuredValue === null) { + delete body[key]; + } else if (configuredValue !== undefined) { + body[key] = configuredValue; + } + } + + return body; } /** diff --git a/extensions/copilot/src/extension/byok/node/test/openAIEndpoint.spec.ts b/extensions/copilot/src/extension/byok/node/test/openAIEndpoint.spec.ts index b9b9eed5184d4..e8716894eff28 100644 --- a/extensions/copilot/src/extension/byok/node/test/openAIEndpoint.spec.ts +++ b/extensions/copilot/src/extension/byok/node/test/openAIEndpoint.spec.ts @@ -7,8 +7,8 @@ import { Raw } from '@vscode/prompt-tsx'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ChatFetchResponseType, ChatResponse } from '../../../../platform/chat/common/commonTypes'; import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; -import { CustomDataPartMimeTypes } from '../../../../platform/endpoint/common/endpointTypes'; import { IChatModelInformation, ModelSupportedEndpoint } from '../../../../platform/endpoint/common/endpointProvider'; +import { CustomDataPartMimeTypes } from '../../../../platform/endpoint/common/endpointTypes'; import { ChatEndpoint } from '../../../../platform/endpoint/node/chatEndpoint'; import { ICreateEndpointBodyOptions, IEndpointBody, IMakeChatRequestOptions } from '../../../../platform/networking/common/networking'; import { ITestingServicesAccessor } from '../../../../platform/test/node/services'; @@ -246,9 +246,19 @@ describe('OpenAIEndpoint - Reasoning Properties', () => { describe('Responses API mode (useResponsesApi = true)', () => { it('should preserve reasoning object when thinking is supported', () => { - accessor.get(IConfigurationService).setConfig(ConfigKey.ResponsesApiReasoningSummary, 'detailed'); + const modelWithReasoningEffort = { + ...modelMetadata, + capabilities: { + ...modelMetadata.capabilities, + supports: { + ...modelMetadata.capabilities.supports, + reasoning_effort: ['low', 'medium', 'high'] + } + } + }; + const endpoint = instaService.createInstance(OpenAIEndpoint, - modelMetadata, + modelWithReasoningEffort, 'test-api-key', 'https://api.openai.com/v1/chat/completions'); @@ -275,7 +285,6 @@ describe('OpenAIEndpoint - Reasoning Properties', () => { } }; - accessor.get(IConfigurationService).setConfig(ConfigKey.ResponsesApiReasoningSummary, 'detailed'); const endpoint = instaService.createInstance(OpenAIEndpoint, modelWithoutThinking, 'test-api-key', diff --git a/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts index 6e32e34a46050..c160d03e02574 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import { CancellationToken, LanguageModelChatMessage, LanguageModelChatMessage2, LanguageModelResponsePart2, Progress, ProvideLanguageModelChatResponseOptions } from 'vscode'; import { AzureAuthMode, ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; -import { isEndpointEditToolName } from '../../../platform/endpoint/common/endpointProvider'; +import { isEndpointEditToolName, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; @@ -45,6 +45,29 @@ export function resolveAzureUrl(modelId: string, url: string): string { } } +/** + * Determines the `supported_endpoints` for a resolved Azure BYOK URL. When the URL targets the + * Responses API, both Chat Completions and Responses are advertised so a Responses-shaped body + * (`input`) is sent; otherwise `undefined` preserves the default Chat Completions behavior. This + * keeps the Entra auth path consistent with the API-key path (issue #318114). + */ +export function azureSupportedEndpointsForUrl(url: string): ModelSupportedEndpoint[] | undefined { + let pathname: string; + try { + pathname = new URL(url).pathname; + } catch { + // Tolerate malformed URLs by preserving the default Chat Completions behavior. + return undefined; + } + // Match the final path segment so a deployment named "responses" (e.g. + // `/openai/deployments/responses/chat/completions`) is not misclassified. Compare + // case-insensitively to tolerate APIM vanity routes that may use different casing. + if (pathname.replace(/\/$/, '').toLowerCase().endsWith('/responses')) { + return [ModelSupportedEndpoint.ChatCompletions, ModelSupportedEndpoint.Responses]; + } + return undefined; +} + export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { public static readonly providerName = 'Azure'; @@ -119,6 +142,13 @@ export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled }; const modelInfo = resolveModelInfo(model.id, this._name, undefined, modelCapabilities); + // Mirror the API-key path (customOAIProvider.createOpenAIEndPoint): when the resolved Azure URL + // targets the Responses API, mark the endpoint accordingly so a Responses-shaped body (`input`) is + // sent instead of a Chat Completions body (`messages`), which Azure Responses rejects (issue #318114). + const supportedEndpoints = azureSupportedEndpointsForUrl(url); + if (supportedEndpoints) { + modelInfo.supported_endpoints = supportedEndpoints; + } const openAIChatEndpoint = this._instantiationService.createInstance( AzureOpenAIEndpoint, diff --git a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts index db5df5f555f46..ea5896d727974 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts @@ -6,7 +6,7 @@ import { IChatMLFetcher } from '../../../platform/chat/common/chatMLFetcher'; import { IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IDomainService } from '../../../platform/endpoint/common/domainService'; -import { EndpointEditToolName, IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; +import { EndpointEditToolName, IChatModelInformation, IChatModelRequestOptions, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IChatWebSocketManager } from '../../../platform/networking/node/chatWebSocketManager'; @@ -98,6 +98,7 @@ interface _CustomEndpointModelConfig { streaming?: boolean; editTools?: EndpointEditToolName[]; requestHeaders?: Record; + modelOptions?: IChatModelRequestOptions; zeroDataRetentionEnabled?: boolean; supportsReasoningEffort?: string[]; reasoningEffortFormat?: 'chat-completions' | 'responses'; @@ -159,6 +160,7 @@ export class CustomEndpointBYOKModelProvider extends AbstractOpenAICompatibleLMP thinking: modelConfiguration?.thinking ?? false, streaming: modelConfiguration?.streaming, requestHeaders: modelConfiguration?.requestHeaders, + modelOptions: modelConfiguration?.modelOptions, zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled, supportsReasoningEffort: modelConfiguration?.supportsReasoningEffort, reasoningEffortFormat: modelConfiguration?.reasoningEffortFormat diff --git a/extensions/copilot/src/extension/byok/vscode-node/openAIProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/openAIProvider.ts index cc10693357616..27df5d409e37f 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/openAIProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/openAIProvider.ts @@ -9,10 +9,22 @@ import { IFetcherService } from '../../../platform/networking/common/fetcherServ import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { BYOKKnownModels } from '../common/byokProvider'; -import { AbstractOpenAICompatibleLMProvider } from './abstractLanguageModelChatProvider'; +import { OpenAIEndpoint } from '../node/openAIEndpoint'; +import { AbstractOpenAICompatibleLMProvider, LanguageModelChatConfiguration, OpenAICompatibleLanguageModelChatInformation } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; -export class OAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider { +export interface OpenAIProviderConfig extends LanguageModelChatConfiguration { + readonly zeroDataRetentionEnabled?: boolean; +} + +export function applyOpenAIProviderConfig(modelInfo: IChatModelInformation, configuration: OpenAIProviderConfig | undefined): IChatModelInformation { + return { + ...modelInfo, + zeroDataRetentionEnabled: configuration?.zeroDataRetentionEnabled ?? modelInfo.zeroDataRetentionEnabled, + }; +} + +export class OAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider { public static readonly providerName = 'OpenAI'; public static readonly providerId = this.providerName.toLowerCase(); @@ -43,6 +55,14 @@ export class OAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider { return 'https://api.openai.com/v1'; } + protected override async createOpenAIEndPoint(model: OpenAICompatibleLanguageModelChatInformation): Promise { + const modelInfo = applyOpenAIProviderConfig(this.getModelInfo(model.id, model.url), model.configuration); + const url = modelInfo.supported_endpoints?.includes(ModelSupportedEndpoint.Responses) ? + `${model.url}/responses` : + `${model.url}/chat/completions`; + return this._instantiationService.createInstance(OpenAIEndpoint, modelInfo, model.configuration?.apiKey ?? '', url); + } + protected override getModelInfo(modelId: string, modelUrl: string): IChatModelInformation { const modelInfo = super.getModelInfo(modelId, modelUrl); modelInfo.supported_endpoints = [ diff --git a/extensions/copilot/src/extension/byok/vscode-node/test/azureProvider.spec.ts b/extensions/copilot/src/extension/byok/vscode-node/test/azureProvider.spec.ts index 36722b25890c7..54527b9c53164 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/test/azureProvider.spec.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/test/azureProvider.spec.ts @@ -5,10 +5,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BlockedExtensionService, IBlockedExtensionService } from '../../../../platform/chat/common/blockedExtensionService'; +import { ModelSupportedEndpoint } from '../../../../platform/endpoint/common/endpointProvider'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; -import { resolveAzureUrl } from '../azureProvider'; +import { azureSupportedEndpointsForUrl, resolveAzureUrl } from '../azureProvider'; describe('AzureBYOKModelProvider', () => { const disposables = new DisposableStore(); @@ -62,10 +63,36 @@ describe('AzureBYOKModelProvider', () => { expect(result).toBe('https://my-endpoint.models.ai.azure.com/v1/chat/completions'); }); + it('should preserve an explicit APIM /responses URL behind a vanity domain', () => { + const url = 'https://my-apim.azure-api.net/openai/responses?api-version=2025-04-01-preview'; + const result = resolveAzureUrl('gpt-4', url); + expect(result).toBe(url); + }); + it('should throw error for unrecognized Azure URL', () => { const url = 'https://unknown.example.com'; expect(() => resolveAzureUrl('gpt-4', url)).toThrow('Unrecognized Azure deployment URL'); }); }); + describe('azureSupportedEndpointsForUrl', () => { + it('marks Responses (and Chat Completions) for /responses URLs and leaves Chat Completions URLs unmarked', () => { + expect({ + responses: azureSupportedEndpointsForUrl('https://my-resource.openai.azure.com/openai/responses?api-version=2025-04-01-preview'), + apimResponses: azureSupportedEndpointsForUrl('https://my-apim.azure-api.net/openai/responses'), + mixedCaseResponses: azureSupportedEndpointsForUrl('https://my-apim.azure-api.net/openai/Responses'), + chatCompletions: azureSupportedEndpointsForUrl('https://my-resource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2025-01-01-preview'), + deploymentNamedResponses: azureSupportedEndpointsForUrl('https://my-resource.openai.azure.com/openai/deployments/responses/chat/completions?api-version=2025-01-01-preview'), + malformed: azureSupportedEndpointsForUrl('not a url'), + }).toEqual({ + responses: [ModelSupportedEndpoint.ChatCompletions, ModelSupportedEndpoint.Responses], + apimResponses: [ModelSupportedEndpoint.ChatCompletions, ModelSupportedEndpoint.Responses], + mixedCaseResponses: [ModelSupportedEndpoint.ChatCompletions, ModelSupportedEndpoint.Responses], + chatCompletions: undefined, + deploymentNamedResponses: undefined, + malformed: undefined, + }); + }); + }); + }); diff --git a/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts b/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts index 9a3197f855e50..0b62c12267347 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts @@ -6,6 +6,7 @@ import { Raw } from '@vscode/prompt-tsx'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { BlockedExtensionService, IBlockedExtensionService } from '../../../../platform/chat/common/blockedExtensionService'; +import { ChatLocation } from '../../../../platform/chat/common/commonTypes'; import { IChatModelInformation, ModelSupportedEndpoint } from '../../../../platform/endpoint/common/endpointProvider'; import { ITestingServicesAccessor } from '../../../../platform/test/node/services'; import { TokenizerType } from '../../../../util/common/tokenizer'; @@ -278,6 +279,180 @@ describe('CustomEndpointBYOKModelProvider', () => { expect(endpoint.ownsAuthorization).toBe(true); }); + it('issue #321514: applies configured model options over default sampling parameters', () => { + const metadata: IChatModelInformation = { + ...makeMetadata(undefined), + modelOptions: { + temperature: 1, + top_p: 0.95, + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + 'https://api.example.com/v1/chat/completions'); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: 'test-req-custom-model-options', + postOptions: { + temperature: 0.1, + top_p: 1, + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + expect({ + temperature: body.temperature, + topP: body.top_p, + }).toEqual({ + temperature: 1, + topP: 0.95, + }); + }); + + it('omits sampling parameters configured as null', () => { + const metadata: IChatModelInformation = { + ...makeMetadata(undefined), + modelOptions: { + temperature: null, + top_p: null, + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + 'https://api.example.com/v1/chat/completions'); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: 'test-req-omitted-model-options', + postOptions: { + temperature: 0.1, + top_p: 1, + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + expect({ + temperature: body.temperature, + topP: body.top_p, + }).toEqual({ + temperature: undefined, + topP: undefined, + }); + }); + + it('keeps explicit per-request sampling parameters ahead of configured model options', () => { + const metadata: IChatModelInformation = { + ...makeMetadata(undefined), + modelOptions: { + temperature: 1, + top_p: null, + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + 'https://api.example.com/v1/chat/completions'); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: 'test-req-explicit-model-options', + requestOptions: { + temperature: 0.7, + top_p: 0.9, + }, + postOptions: { + temperature: 0.1, + top_p: 1, + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + expect({ + temperature: body.temperature, + topP: body.top_p, + }).toEqual({ + temperature: 0.7, + topP: 0.9, + }); + }); + + it('applies configured model options to Responses and Messages API bodies', () => { + const results = [ + { + supportedEndpoints: [ModelSupportedEndpoint.Responses], + url: 'https://api.example.com/v1/responses', + }, + { + supportedEndpoints: [ModelSupportedEndpoint.Messages], + url: 'https://api.example.com/v1/messages', + }, + ].map(({ supportedEndpoints, url }) => { + const metadata: IChatModelInformation = { + ...makeMetadata(supportedEndpoints), + modelOptions: { + temperature: 1, + top_p: 0.95, + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + url); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: `test-req-${endpoint.apiType}-model-options`, + postOptions: { + temperature: 0.1, + top_p: 1, + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + return { + apiType: endpoint.apiType, + temperature: body.temperature, + topP: body.top_p, + }; + }); + + expect(results).toEqual([ + { + apiType: 'responses', + temperature: 1, + topP: 0.95, + }, + { + apiType: 'messages', + temperature: 1, + topP: 0.95, + }, + ]); + }); + it('replaces default Bearer with user-supplied Authorization header on Chat Completions endpoints', () => { const metadata = makeMetadata(undefined); metadata.requestHeaders = { 'Authorization': 'Bearer user-token' }; diff --git a/extensions/copilot/src/extension/byok/vscode-node/test/openAIProvider.spec.ts b/extensions/copilot/src/extension/byok/vscode-node/test/openAIProvider.spec.ts new file mode 100644 index 0000000000000..b56047aee9313 --- /dev/null +++ b/extensions/copilot/src/extension/byok/vscode-node/test/openAIProvider.spec.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from 'vitest'; +import { IChatModelInformation } from '../../../../platform/endpoint/common/endpointProvider'; +import { TokenizerType } from '../../../../util/common/tokenizer'; +import { applyOpenAIProviderConfig } from '../openAIProvider'; + +function createModelInfo(zeroDataRetentionEnabled: boolean | undefined): IChatModelInformation { + return { + id: 'gpt-4.1', + name: 'GPT-4.1', + vendor: 'OpenAI', + version: '1.0.0', + is_chat_default: false, + is_chat_fallback: false, + model_picker_enabled: true, + zeroDataRetentionEnabled, + capabilities: { + type: 'chat', + family: 'gpt-4.1', + supports: { + streaming: true, + tool_calls: true, + vision: false, + thinking: false, + }, + tokenizer: TokenizerType.O200K, + limits: { + max_context_window_tokens: 128000, + max_prompt_tokens: 100000, + max_output_tokens: 8192, + }, + }, + }; +} + +describe('applyOpenAIProviderConfig', () => { + it('uses provider-level zeroDataRetentionEnabled when configured', () => { + const merged = applyOpenAIProviderConfig(createModelInfo(undefined), { + apiKey: 'test-key', + zeroDataRetentionEnabled: true, + }); + + expect(merged.zeroDataRetentionEnabled).toBe(true); + }); + + it('falls back to model metadata zeroDataRetentionEnabled when provider-level value is unset', () => { + const merged = applyOpenAIProviderConfig(createModelInfo(true), { + apiKey: 'test-key', + }); + + expect(merged.zeroDataRetentionEnabled).toBe(true); + }); +}); diff --git a/extensions/copilot/src/extension/chatInputNotification/vscode-node/byokUtilityModel.contribution.ts b/extensions/copilot/src/extension/chatInputNotification/vscode-node/byokUtilityModel.contribution.ts index 8b282e80b4ee7..eb6d53d172747 100644 --- a/extensions/copilot/src/extension/chatInputNotification/vscode-node/byokUtilityModel.contribution.ts +++ b/extensions/copilot/src/extension/chatInputNotification/vscode-node/byokUtilityModel.contribution.ts @@ -16,9 +16,8 @@ const UTILITY_SMALL_MODEL_SETTING = 'chat.utilitySmallModel'; /** * Shows a chat input notification in air-gapped BYOK scenarios (no GitHub * session) when at least one BYOK model is available but the utility model - * settings are still defaults. The default utility models require GitHub - * Copilot access, so without it the utility slots silently fall back and - * degrade the experience until the user points them at a BYOK model. + * settings are still defaults. Some utility flows are unavailable until the + * user points them at a BYOK model. * * The notification hides automatically once the user signs in, BYOK models * disappear, or both utility settings are configured. diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.ts index 9acd2aa786d68..fe11ad14a684b 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.ts @@ -610,7 +610,7 @@ export function buildChatHistoryFromEvents(sessionId: string, modelId: string | } }); ((event.data.attachments || [])) - .filter(attachment => attachment.type === 'selection' || attachment.type === 'github_reference' || attachment.type === 'blob' || attachment.type === 'extension_context' ? true : !isInstructionAttachmentPath(attachment.path)) + .filter(attachment => attachment.type === 'selection' || attachment.type === 'github_reference' || attachment.type === 'blob' || attachment.type === 'github_actions_job' || attachment.type === 'github_url' || attachment.type === 'github_tree_comparison' || attachment.type === 'github_release' || attachment.type === 'github_repository' || attachment.type === 'github_file_diff' || attachment.type === 'github_commit' || attachment.type === 'github_file' || attachment.type === 'extension_context' ? true : !isInstructionAttachmentPath(attachment.path)) .forEach(attachment => { if (attachment.type === 'github_reference') { return; @@ -643,9 +643,13 @@ export function buildChatHistoryFromEvents(sessionId: string, modelId: string | range }); } else if (attachment.type === 'blob') { + if (typeof attachment.data !== 'string') { + return; + } + const data = attachment.data; const binaryDataSupplier = async () => { try { - return decodeBase64(attachment.data).buffer; + return decodeBase64(data).buffer; } catch (error) { logger.error(error, `Failed to decode blob attachment ${attachment.displayName || ''}`); throw error; @@ -1260,6 +1264,9 @@ function formatSearchToolInvocationCompleted(invocation: ChatToolInvocationPart, } const searchInPath = toolCall.arguments.path ? ` in \`${toolCall.arguments.path}\`` : ''; let files: string[] = []; + // TODO: Revisit this legacy SDK terminal content usage. SDK `terminal` + // content is deprecated for shell command exit metadata; this search + // result parsing should not rely on it long term. if (Array.isArray(result.result?.contents) && result.result.contents.length > 0 && result.result.contents[0].type === 'terminal' && typeof result.result.contents[0].text === 'string') { const matches = result.result.contents[0].text.trim(); const noMatches = matches.length === 0; diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts index 87c261072f795..bce3ca26dd4f9 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts @@ -26,7 +26,7 @@ import { IInstantiationService } from '../../../../util/vs/platform/instantiatio import { ensureNodePtyShim } from './nodePtyShim'; import { ensureRipgrepShim } from './ripgrepShim'; import { CancellationToken } from '../../../../util/vs/base/common/cancellation'; -import { formatTokenCount, getModelCapabilitiesDescription, getReasoningEffortDescription, normalizeTokenPrices } from '../../../conversation/common/languageModelAccess'; +import { formatTokenCount, getAutoModelDescription, getModelCapabilitiesDescription, getReasoningEffortDescription, normalizeTokenPrices } from '../../../conversation/common/languageModelAccess'; export const COPILOT_CLI_REASONING_EFFORT_PROPERTY = 'reasoningEffort'; const COPILOT_CLI_MODEL_MEMENTO_KEY = 'github.copilot.cli.sessionModel'; @@ -60,6 +60,7 @@ export interface CopilotCLIModelInfo { readonly supportsReasoningEffort?: boolean; readonly defaultReasoningEffort?: string; readonly supportedReasoningEfforts?: string[]; + readonly warningText?: Record; } export interface ICopilotCLIModels { @@ -252,6 +253,7 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels { toolCalling: true }, targetChatSessionType: 'copilotcli', + warningText: model.warningText, isDefault: !isAutoModelEnabled && index === 0 ? true : undefined, }; const tooltip = getModelCapabilitiesDescription(modelInfo) ?? ''; @@ -271,7 +273,7 @@ function buildAutoModel(defaultModel?: CopilotCLIModelInfo): vscode.LanguageMode return { id: 'auto', name: 'Auto', - tooltip: l10n.t('Auto selects the best model based on your request complexity and model performance.'), + tooltip: getAutoModelDescription(), family: defaultModel?.id ?? '', version: '', maxInputTokens: defaultModel?.maxInputTokens ?? defaultModel?.maxContextWindowTokens ?? 0, @@ -315,20 +317,33 @@ function buildConfigurationSchema(modelInfo: CopilotCLIModelInfo, isReasoningEff if (defaultContextMax && defaultContextMax < fullMax) { const hasLongContextSurcharge = modelInfo.longContextInputCost !== undefined || modelInfo.longContextOutputCost !== undefined; - properties[COPILOT_CLI_CONTEXT_SIZE_PROPERTY] = { - type: 'number', - title: l10n.t('Context Size'), - enum: [defaultContextMax, fullMax], - enumItemLabels: [formatTokenCount(defaultContextMax), formatTokenCount(fullMax)], - enumDescriptions: [ - l10n.t('Default'), - hasLongContextSurcharge - ? l10n.t('Longer sessions') - : l10n.t('Longer sessions without compaction'), - ], - default: defaultContextMax, - group: 'tokens', - }; + if (hasLongContextSurcharge) { + properties[COPILOT_CLI_CONTEXT_SIZE_PROPERTY] = { + type: 'number', + title: l10n.t('Context Size'), + enum: [defaultContextMax, fullMax], + enumItemLabels: [formatTokenCount(defaultContextMax), formatTokenCount(fullMax)], + enumDescriptions: [ + l10n.t('Default'), + l10n.t('Longer sessions'), + ], + default: defaultContextMax, + group: 'tokens', + }; + } else { + // No surcharge — show only the long context option as a non-switchable indicator. + properties[COPILOT_CLI_CONTEXT_SIZE_PROPERTY] = { + type: 'number', + title: l10n.t('Context Size'), + enum: [fullMax], + enumItemLabels: [formatTokenCount(fullMax)], + enumDescriptions: [ + l10n.t('Longer sessions'), + ], + default: fullMax, + group: 'tokens', + }; + } } if (Object.keys(properties).length === 0) { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts index 6e0634f28de55..ca6ed26a50c05 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts @@ -911,7 +911,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes private readonly _agentName: string | undefined, private readonly _sdkSession: Session, private readonly _additionalWorkspaces: IWorkspaceInfo[], - private readonly _sandboxEnabled: boolean, + private readonly _sandboxConfig: SessionOptions['sandboxConfig'], @ILogService private readonly logService: ILogService, @IWorkspaceService private readonly workspaceService: IWorkspaceService, @IChatSessionMetadataStore private readonly _chatSessionMetadataStore: IChatSessionMetadataStore, @@ -943,6 +943,33 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes this._permissionLevel = level; } + /** + * Whether the session was configured with the sandbox enabled. The sandbox + * only actually applies to requests that run with default approvals — see + * {@link _applyEffectiveSandboxConfig}. + */ + private get _sandboxEnabled(): boolean { + return !!this._sandboxConfig?.enabled; + } + + /** + * Apply the sandbox policy for the request that is about to be sent. The + * sandbox enable setting only applies under default approvals; the sandbox + * is explicitly disabled when the request runs with bypass approvals + * (autopilot / autoApprove) or when no sandbox is configured for the + * session. Pushing `{ enabled: false }` (rather than skipping the update) + * ensures the SDK never retains a stale or auto-discovered sandbox. + */ + private _applyEffectiveSandboxConfig(bypassApprovals: boolean): void { + const base = this._sandboxConfig; + const sandboxConfig = (base?.enabled && !bypassApprovals) ? base : { enabled: false }; + try { + this._sdkSession.updateOptions({ sandboxConfig }); + } catch (error) { + this.logService.error(error, '[CopilotCLISession] Failed to update sandbox config for request'); + } + } + // TODO: This should be pre-populated when we restore a session based on its original context. // E.g. if we're resuming a session, and it tries to read a file, we shouldn't prompt for permissions again. /** @@ -1276,14 +1303,16 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes const permissionRequest = event.data.permissionRequest; const requestId = event.data.requestId; + const isSandboxBypassShell = permissionRequest.kind === 'shell' && permissionRequest.requestSandboxBypass === true; + // Auto-approve all requests when the permission level allows it. - if (effectivePermissionLevel === 'autoApprove' || effectivePermissionLevel === 'autopilot') { + if (!isSandboxBypassShell && (effectivePermissionLevel === 'autoApprove' || effectivePermissionLevel === 'autopilot')) { this.logService.trace(`[CopilotCLISession] Auto Approving ${permissionRequest.kind} request (permission level: ${effectivePermissionLevel})`); this._sdkSession.respondToPermission(requestId, { kind: 'approve-once' }); return; } - if (permissionRequest.kind === 'shell' && this._sandboxEnabled) { + if (!isSandboxBypassShell && permissionRequest.kind === 'shell' && this._sandboxEnabled) { this.logService.trace(`[CopilotCLISession] Auto Approving shell request (sandbox is enabled)`); this._sdkSession.respondToPermission(requestId, { kind: 'approve-once' }); return; @@ -1328,7 +1357,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes try { let response: PermissionRequestResult; - if (effectivePermissionLevel === 'autoApprove' || effectivePermissionLevel === 'autopilot') { + if (!isSandboxBypassShell && (effectivePermissionLevel === 'autoApprove' || effectivePermissionLevel === 'autopilot')) { this.logService.trace(`[CopilotCLISession] Auto Approving ${permissionRequest.kind} request (permission level: ${effectivePermissionLevel})`); response = { kind: 'approve-once' }; } else if (this._mcState) { @@ -1435,7 +1464,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes reportUsage(event.data.inputTokens, event.data.outputTokens); } // Accumulate per-turn credits from SDK copilotUsage data - const copilotUsage = (event.data as Record).copilotUsage; + const copilotUsage = (event.data as unknown as Record).copilotUsage; let copilotUsageNanoAiu: number | undefined; if (copilotUsage && typeof copilotUsage === 'object') { const { totalNanoAiu } = copilotUsage as { totalNanoAiu?: number }; @@ -1859,6 +1888,12 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes } else { this._sdkSession.currentMode = 'interactive'; } + // The sandbox only applies under default approvals — disable it for + // this request when running in a bypass-approvals mode. + const bypassApprovals = remoteMode + ? remoteMode === 'autopilot' + : this._permissionLevel === 'autopilot' || this._permissionLevel === 'autoApprove'; + this._applyEffectiveSandboxConfig(bypassApprovals); const sendOptions: SendOptions = { prompt: input.prompt ?? '', attachments, agentMode: this._sdkSession.currentMode }; if (steering) { sendOptions.mode = 'immediate'; @@ -1896,6 +1931,9 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes } else { this._sdkSession.currentMode = 'interactive'; } + // The sandbox only applies under default approvals — disable it when + // fleet runs in autopilot (a bypass-approvals mode). + this._applyEffectiveSandboxConfig(this._permissionLevel === 'autopilot'); const result = await this._sdkSession.fleet.start({ prompt }); if (!result.started) { this.logService.info('[CopilotCLISession] Fleet mode not started'); @@ -2741,14 +2779,58 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes private _renderAttachments(attachments: Attachment[]): string[] { const lines: string[] = []; for (const attachment of attachments) { - if (attachment.type === 'github_reference') { - lines.push(`- ${attachment.title}: (${attachment.number}, ${attachment.type}, ${attachment.referenceType})`); - } else if (attachment.type === 'blob') { - lines.push(`- ${attachment.displayName ?? 'blob'} (${attachment.type}, ${attachment.mimeType})`); - } else if (attachment.type === 'extension_context') { - lines.push(`- ${attachment.title ?? 'extension_context'} (${attachment.type}, ${attachment.extensionId})`); - } else { - lines.push(`- ${attachment.displayName} (${attachment.type}, ${attachment.type === 'selection' ? attachment.filePath : attachment.path})`); + switch (attachment.type) { + case 'github_reference': { + lines.push(`- ${attachment.title}: (${attachment.number}, ${attachment.type}, ${attachment.referenceType})`); + break; + } + case 'github_actions_job': { + lines.push(`- ${attachment.jobName}: (${attachment.jobId}, ${attachment.type})`); + break; + } + case 'github_commit': { + lines.push(`- ${attachment.message}: (${attachment.oid}, ${attachment.type})`); + break; + } + case 'github_file': { + lines.push(`- ${attachment.path}: (${attachment.ref}, ${attachment.type})`); + break; + } + case 'github_file_diff': { + lines.push(`- ${attachment.url}: (${attachment.type})`); + break; + } + case 'github_release': { + lines.push(`- ${attachment.name}: (${attachment.tagName}, ${attachment.type})`); + break; + } + case 'github_repository': { + lines.push(`- ${attachment.repo.name}: (${attachment.url}, ${attachment.type})`); + break; + } + case 'github_tree_comparison': { + lines.push(`- ${attachment.head}: (${attachment.base}, ${attachment.type})`); + break; + } + case 'github_url': { + lines.push(`- ${attachment.url}: (${attachment.type})`); + break; + } + case 'github_snippet': { + lines.push(`- ${attachment.path}: (${attachment.type})`); + break; + } + case 'blob': { + lines.push(`- ${attachment.displayName ?? 'blob'} (${attachment.type}, ${attachment.mimeType})`); + break; + } + case 'extension_context': { + lines.push(`- ${attachment.title ?? 'extension_context'} (${attachment.type}, ${attachment.extensionId})`); + break; + } + default: { + lines.push(`- ${attachment.displayName} (${attachment.type}, ${attachment.type === 'selection' ? attachment.filePath : attachment.path})`); + } } } return lines; diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts index c2e2c427b123d..3c47edec8ce05 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts @@ -856,7 +856,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS } this.logService.trace(`[CopilotCLISession] Created new CopilotCLI session ${sdkSession.sessionId}.`); - const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager, !!sessionOptions.sandboxConfig?.enabled); + const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager, sessionOptions.sandboxConfig); session.object.add(mcpGateway); // Set origin @@ -1086,7 +1086,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS return undefined; } await sessionManager.loadDeferredRepoHooks(sdkSession.sessionId); - const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager, !!sessionOptions.sandboxConfig?.enabled); + const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager, sessionOptions.sandboxConfig); session.object.add(mcpGateway); return session; } @@ -1366,9 +1366,9 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS return firstUserMessage; } - private createCopilotSession(sdkSession: Session, workspaceInfo: IWorkspaceInfo, agentName: string | undefined, sessionManager: internal.LocalSessionManager, sandboxEnabled: boolean): RefCountedSession { + private createCopilotSession(sdkSession: Session, workspaceInfo: IWorkspaceInfo, agentName: string | undefined, sessionManager: internal.LocalSessionManager, sandboxConfig: SessionOptions['sandboxConfig']): RefCountedSession { sdkSession.permissions.setRequired({ required: true }); - const session = this.instantiationService.createInstance(CopilotCLISession, workspaceInfo, agentName, sdkSession, [], sandboxEnabled); + const session = this.instantiationService.createInstance(CopilotCLISession, workspaceInfo, agentName, sdkSession, [], sandboxConfig); this._debugFileLogger.startSession(session.sessionId).catch(err => { this.logService.error('[CopilotCLISession] Failed to start debug log session', err); }); @@ -1652,6 +1652,15 @@ export interface IAgentSandboxFileSystemSettings { windows?: IAgentSandboxFileSystemSetting; } +/** + * Whether the CLI sandbox is supported on Windows. Not enabled yet, so + * {@link buildSandboxConfigForCLI} bails out early on `win32`; the Windows + * handling is kept so support can be turned on by flipping this flag once the + * runtime is ready. Typed as `boolean` (not the `false` literal) so the Windows + * branch is not flagged as unreachable by control-flow narrowing. + */ +const WINDOWS_SANDBOX_SUPPORTED: boolean = false; + /** * Maps the workbench `chat.agent.sandbox.*` settings onto the SDK's * {@link SessionOptions.sandboxConfig}. @@ -1660,6 +1669,11 @@ export interface IAgentSandboxFileSystemSettings { * setting wins: `denyRead` > `denyWrite` > `allowWrite` > `allowRead`. Each * path is emitted in exactly one of `deniedPaths` / `readonlyPaths` / * `readwritePaths`, regardless of how many settings reference it. + * + * Windows is not supported yet, so this bails out early and returns `undefined` + * there. The Windows handling below is intentionally kept (and exercised when + * {@link WINDOWS_SANDBOX_SUPPORTED} is flipped) so support can be turned on once + * the runtime is ready. */ export function buildSandboxConfigForCLI( platform: NodeJS.Platform, @@ -1672,6 +1686,12 @@ export function buildSandboxConfigForCLI( return undefined; } + // Typed as `boolean` (not the `false` literal) so the Windows branch below + // is not flagged as unreachable by control-flow narrowing. + if (platform === 'win32' && !WINDOWS_SANDBOX_SUPPORTED) { + return undefined; + } + const fs = (platform === 'win32' ? fileSystemSetting?.windows : platform === 'darwin' @@ -1703,17 +1723,17 @@ export function buildSandboxConfigForCLI( // the host filter is actually enforced (the SDK strips host lists when outbound is off). A // deny-list-only configuration still permits non-denied domains. // - // macOS Seatbelt has no per-host filter — the runtime strips both lists and would silently - // degrade `allowOutbound: true` to "allow all outbound". To avoid surprising the user with - // unrestricted access when they explicitly configured host rules, fail closed on darwin: keep - // outbound off and drop the unenforceable lists. + // Host lists are currently disabled on all platforms: the runtime does not yet enforce them + // reliably everywhere, so we fail closed — keep outbound off and drop the host lists — to avoid + // surprising the user with unrestricted access when they explicitly configured host rules. const allowAllNetwork = sandboxSetting === 'allowNetwork'; - const hostListsEnforceable = platform !== 'darwin'; + const hostListsEnforceable = false; const allowedHosts = !allowAllNetwork && hostListsEnforceable && networkHosts?.allowedHosts?.length ? [...networkHosts.allowedHosts] : undefined; const blockedHosts = !allowAllNetwork && hostListsEnforceable && networkHosts?.blockedHosts?.length ? [...networkHosts.blockedHosts] : undefined; const allowOutbound = allowAllNetwork || !!allowedHosts || !!blockedHosts; return { enabled: true, + allowBypass: true, userPolicy: { filesystem: { ...(readwrite.size ? { readwritePaths: [...readwrite] } : {}), diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/permissionHelpers.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/permissionHelpers.ts index 20a47e2e12773..73320476b605c 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/permissionHelpers.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/permissionHelpers.ts @@ -28,6 +28,8 @@ type CoreTerminalConfirmationToolParams = { message: string; command: string | undefined; isBackground: boolean; + sandboxBypass?: boolean; + sandboxBypassReason?: string; }; }; @@ -217,12 +219,21 @@ export function buildShellConfirmationParams( const userFriendlyCommand = fullCommandText ? getCdPresentationOverrides(fullCommandText, isPowershell, workingDirectory)?.commandLine : undefined; const command = userFriendlyCommand ?? fullCommandText; + // When the model opted this command out of the sandbox, surface that to the + // user so the confirmation makes the elevation of privilege clear (the + // terminal confirmation tool renders its own title, so the note must be + // passed as a dedicated flag rather than baked into the message). + const sandboxBypass = permissionRequest.requestSandboxBypass === true; + const sandboxBypassReason = sandboxBypass ? permissionRequest.requestSandboxBypassReason : undefined; + return { tool: ToolName.CoreTerminalConfirmationTool, input: { message: permissionRequest.intention || command || codeBlock(permissionRequest), command, - isBackground: false + isBackground: false, + ...(sandboxBypass ? { sandboxBypass: true } : {}), + ...(sandboxBypassReason ? { sandboxBypassReason } : {}), } }; } @@ -424,7 +435,7 @@ export function getFileBeingEdited(permissionRequest: Extract): string { +function codeBlock(obj: unknown): string { return `\n\n\`\`\`\n${JSON.stringify(obj, null, 2)}\n\`\`\``; } diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/buildSandboxConfigForCLI.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/buildSandboxConfigForCLI.spec.ts index 47bd1da784cbd..5da73bb335b09 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/buildSandboxConfigForCLI.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/buildSandboxConfigForCLI.spec.ts @@ -23,23 +23,31 @@ describe('buildSandboxConfigForCLI', () => { expect(buildSandboxConfigForCLI('win32', 'off', undefined)).toBeUndefined(); }); - it('enables sandbox for `on` on every platform', () => { - for (const platform of ['darwin', 'linux', 'win32'] as const) { + it('enables sandbox for `on` on non-Windows platforms', () => { + for (const platform of ['darwin', 'linux'] as const) { expect(buildSandboxConfigForCLI(platform, 'on', undefined)).toEqual({ enabled: true, + allowBypass: true, userPolicy: { filesystem: {}, network: { allowOutbound: false } }, }); } }); - it('enables sandbox and outbound network for `allowNetwork` on every platform', () => { - for (const platform of ['darwin', 'linux', 'win32'] as const) { + it('enables sandbox and outbound network for `allowNetwork` on non-Windows platforms', () => { + for (const platform of ['darwin', 'linux'] as const) { expect(buildSandboxConfigForCLI(platform, 'allowNetwork', undefined)).toEqual({ enabled: true, + allowBypass: true, userPolicy: { filesystem: {}, network: { allowOutbound: true } }, }); } }); + + it('ignores the enable setting on Windows', () => { + // The sandbox is not supported on Windows, so the enable setting is ignored. + expect(buildSandboxConfigForCLI('win32', 'on', undefined)).toBeUndefined(); + expect(buildSandboxConfigForCLI('win32', 'allowNetwork', undefined)).toBeUndefined(); + }); }); describe('filesystem policy', () => { @@ -51,7 +59,8 @@ describe('buildSandboxConfigForCLI', () => { }; expect(buildSandboxConfigForCLI('linux', 'on', setting)?.userPolicy?.filesystem).toEqual({ readwritePaths: ['/linux'] }); expect(buildSandboxConfigForCLI('darwin', 'on', setting)?.userPolicy?.filesystem).toEqual({ readwritePaths: ['/mac'] }); - expect(buildSandboxConfigForCLI('win32', 'on', setting)?.userPolicy?.filesystem).toEqual({ readwritePaths: ['C:\\win'] }); + // Windows is ignored entirely. + expect(buildSandboxConfigForCLI('win32', 'on', setting)).toBeUndefined(); }); it('maps each setting to the corresponding SDK list', () => { @@ -63,6 +72,7 @@ describe('buildSandboxConfigForCLI', () => { }; expect(buildSandboxConfigForCLI('darwin', 'on', fsFor('darwin', fs))).toEqual({ enabled: true, + allowBypass: true, userPolicy: { filesystem: { readwritePaths: ['/work'], @@ -77,6 +87,7 @@ describe('buildSandboxConfigForCLI', () => { it('omits filesystem lists that are empty', () => { expect(buildSandboxConfigForCLI('darwin', 'on', { mac: {} })).toEqual({ enabled: true, + allowBypass: true, userPolicy: { filesystem: {}, network: { allowOutbound: false } }, }); }); @@ -127,32 +138,20 @@ describe('buildSandboxConfigForCLI', () => { }); describe('network hosts', () => { - it('forwards allowedHosts and opens outbound even when sandbox is `on`', () => { - expect(buildSandboxConfigForCLI('linux', 'on', undefined, { allowedHosts: ['github.com'] })?.userPolicy?.network).toEqual({ - allowOutbound: true, - allowedHosts: ['github.com'], - }); + it('drops host lists and keeps outbound closed when sandbox is `on` (host lists disabled on all platforms)', () => { + for (const platform of ['darwin', 'linux'] as const) { + expect(buildSandboxConfigForCLI(platform, 'on', undefined, { allowedHosts: ['github.com'], blockedHosts: ['evil.example'] })?.userPolicy?.network).toEqual({ + allowOutbound: false, + }); + } }); it('ignores host lists when sandbox is `allowNetwork` (allow all)', () => { - expect(buildSandboxConfigForCLI('linux', 'allowNetwork', undefined, { allowedHosts: ['a.example'], blockedHosts: ['b.example'] })?.userPolicy?.network).toEqual({ - allowOutbound: true, - }); - }); - - it('forwards blockedHosts and opens outbound when only blockedHosts is set (deny-list-only allows non-denied domains)', () => { - expect(buildSandboxConfigForCLI('linux', 'on', undefined, { blockedHosts: ['evil.example'] })?.userPolicy?.network).toEqual({ - allowOutbound: true, - blockedHosts: ['evil.example'], - }); - }); - - it('forwards both allowedHosts and blockedHosts together when sandbox is `on`', () => { - expect(buildSandboxConfigForCLI('linux', 'on', undefined, { allowedHosts: ['a.example'], blockedHosts: ['b.example'] })?.userPolicy?.network).toEqual({ - allowOutbound: true, - allowedHosts: ['a.example'], - blockedHosts: ['b.example'], - }); + for (const platform of ['darwin', 'linux'] as const) { + expect(buildSandboxConfigForCLI(platform, 'allowNetwork', undefined, { allowedHosts: ['a.example'], blockedHosts: ['b.example'] })?.userPolicy?.network).toEqual({ + allowOutbound: true, + }); + } }); it('ignores empty host lists', () => { @@ -160,18 +159,6 @@ describe('buildSandboxConfigForCLI', () => { allowOutbound: false, }); }); - - it('drops host lists and keeps outbound closed on darwin (Seatbelt has no per-host filter)', () => { - expect(buildSandboxConfigForCLI('darwin', 'on', undefined, { allowedHosts: ['github.com'], blockedHosts: ['evil.example'] })?.userPolicy?.network).toEqual({ - allowOutbound: false, - }); - }); - - it('darwin `allowNetwork` still opens outbound (host lists were already ignored)', () => { - expect(buildSandboxConfigForCLI('darwin', 'allowNetwork', undefined, { allowedHosts: ['github.com'] })?.userPolicy?.network).toEqual({ - allowOutbound: true, - }); - }); }); }); diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts index 531be8525c5ea..eabe2168224b4 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts @@ -44,7 +44,7 @@ function buildAutoModel(defaultModel?: CopilotCLIModelInfo): LanguageModelChatIn return { id: 'auto', name: 'Auto', - tooltip: 'Auto selects the best model based on your request complexity and model performance.', + tooltip: 'Auto routes based on your task and real-time system health and model performance. [Learn More](https://docs.github.com/en/copilot/concepts/models/auto-model-selection)', family: defaultModel?.id ?? '', version: '', maxInputTokens: defaultModel?.maxInputTokens ?? defaultModel?.maxContextWindowTokens ?? 0, diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts index 069c4a7206c32..65d2be693fa38 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts @@ -179,7 +179,7 @@ describe('CopilotCLISessionService', () => { deleteSession: vi.fn(async () => { }), }; } - return disposables.add(new CopilotCLISession(workspaceInfo, agentName, sdkSession, [], false, logService, workspaceService, new MockChatSessionMetadataStore(), instantiationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService(), new FakeUserQuestionHandler(), accessor.get(IConfigurationService), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new MockGitService(), { _serviceBrand: undefined } as any, { _serviceBrand: undefined, resetTurnCredits() { }, getCreditsForTurn() { return undefined; }, setLastCopilotUsage() { } } as any, new NullTelemetryService())); + return disposables.add(new CopilotCLISession(workspaceInfo, agentName, sdkSession, [], undefined, logService, workspaceService, new MockChatSessionMetadataStore(), instantiationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService(), new FakeUserQuestionHandler(), accessor.get(IConfigurationService), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new MockGitService(), { _serviceBrand: undefined } as any, { _serviceBrand: undefined, resetTurnCredits() { }, getCreditsForTurn() { return undefined; }, setLastCopilotUsage() { } } as any, new NullTelemetryService())); } } as unknown as IInstantiationService; const configurationService = accessor.get(IConfigurationService); diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts index 27de7188f4c4c..82ba5f455948c 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts @@ -152,10 +152,16 @@ class MockSdkSession { set toolMetadata(value: unknown[] | undefined) { this._toolMetadata = value; } setAuthInfo(info: any) { this.authInfo = info; } - updateOptions(options: { authInfo?: unknown }) { + public lastSandboxConfig: unknown; + public sandboxConfigUpdates: unknown[] = []; + updateOptions(options: { authInfo?: unknown; sandboxConfig?: unknown }) { if (options.authInfo !== undefined) { this.authInfo = options.authInfo; } + if (options.sandboxConfig !== undefined) { + this.lastSandboxConfig = options.sandboxConfig; + this.sandboxConfigUpdates.push(options.sandboxConfig); + } } async getSelectedModel() { return this._selectedModel; } async setSelectedModel(model: string, _reasoningEffort?: string) { this._selectedModel = model; } @@ -257,23 +263,26 @@ describe('CopilotCLISession', () => { }); - async function createSession(): Promise { - return createSessionWith(new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' }))); + async function createSession(opts?: { sandboxEnabled?: boolean }): Promise { + return createSessionWith(new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), opts?.sandboxEnabled ?? false); } - async function createSessionWith(otelService: IOTelService): Promise { + async function createSessionWith(otelService: IOTelService, sandboxEnabled = false): Promise { class FakeUserQuestionHandler implements IUserQuestionHandler { _serviceBrand: undefined; async askUserQuestion(question: IQuestion, toolInvocationToken: ChatParticipantToolToken, token: CancellationToken, toolCallId?: string): Promise { return userQuestionAnswer; } } + const sandboxConfig = sandboxEnabled + ? { enabled: true as const, userPolicy: { filesystem: {}, network: { allowOutbound: false } } } + : undefined; return disposables.add(new CopilotCLISession( sessionWorkspaceInfo, sessionAgentName, sdkSession as unknown as Session, [], - false, + sandboxConfig as any, logger, workspaceService, chatSessionMetadataStore, @@ -852,6 +861,92 @@ describe('CopilotCLISession', () => { await requestPromise; }); + it('auto-approves an ordinary shell command when the sandbox is enabled', async () => { + let result: unknown; + sdkSession.send = async () => { + result = await sdkSession.emitPermissionRequest({ + kind: 'shell', + toolCallId: 'sandboxed-tool', + commands: [{ identifier: 'ls', readOnly: true }], + intention: 'List files', + fullCommandText: 'ls', + possiblePaths: [], + possibleUrls: [], + hasWriteFileRedirection: false, + canOfferSessionApproval: false, + }); + }; + const session = await createSession({ sandboxEnabled: true }); + session.attachStream(new MockChatResponseStream()); + await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Run bash' }, [], undefined, authInfo, CancellationToken.None); + + // The sandbox contains the command, so it is auto-approved without a prompt. + expect(result).toEqual({ kind: 'approve-once' }); + expect(toolsService.invokeToolCalls.filter(c => c.name === 'vscode_get_terminal_confirmation')).toHaveLength(0); + }); + + it('does not auto-approve a sandbox-bypass shell command and flags it runs outside the sandbox', async () => { + let result: unknown; + sdkSession.send = async () => { + result = await sdkSession.emitPermissionRequest({ + kind: 'shell', + toolCallId: 'bypass-tool', + commands: [{ identifier: 'cat ~/secret', readOnly: true }], + intention: 'Read secret', + fullCommandText: 'cat ~/secret', + possiblePaths: [], + possibleUrls: [], + hasWriteFileRedirection: false, + canOfferSessionApproval: false, + requestSandboxBypass: true, + requestSandboxBypassReason: 'Needs access outside the workspace', + }); + }; + toolsService.setConfirmationResult('yes'); + const session = await createSession({ sandboxEnabled: true }); + session.attachStream(new MockChatResponseStream()); + await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Run bash' }, [], undefined, authInfo, CancellationToken.None); + + // Opting out of the sandbox is an elevation of privilege, so it must fall + // through to the confirmation prompt rather than being auto-approved, and + // the confirmation must be flagged as running outside the sandbox. + const confirmationCalls = toolsService.invokeToolCalls.filter(c => c.name === 'vscode_get_terminal_confirmation'); + expect(confirmationCalls).toHaveLength(1); + expect(confirmationCalls[0].input as { sandboxBypass?: boolean; sandboxBypassReason?: string }).toMatchObject({ + sandboxBypass: true, + sandboxBypassReason: 'Needs access outside the workspace', + }); + expect(result).toEqual({ kind: 'approve-once' }); + }); + + it('applies the configured sandbox for a request running with default approvals', async () => { + const session = await createSession({ sandboxEnabled: true }); + session.attachStream(new MockChatResponseStream()); + await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Run' }, [], undefined, authInfo, CancellationToken.None); + + expect(sdkSession.lastSandboxConfig).toEqual({ enabled: true, userPolicy: { filesystem: {}, network: { allowOutbound: false } } }); + }); + + it('disables the sandbox for a request running with bypass approvals', async () => { + for (const level of ['autopilot', 'autoApprove'] as const) { + sdkSession = new MockSdkSession(); + const session = await createSession({ sandboxEnabled: true }); + session.setPermissionLevel(level); + session.attachStream(new MockChatResponseStream()); + await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Run' }, [], undefined, authInfo, CancellationToken.None); + + expect(sdkSession.lastSandboxConfig, level).toEqual({ enabled: false }); + } + }); + + it('explicitly disables the sandbox when the session has no sandbox configured', async () => { + const session = await createSession({ sandboxEnabled: false }); + session.attachStream(new MockChatResponseStream()); + await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Run' }, [], undefined, authInfo, CancellationToken.None); + + expect(sdkSession.lastSandboxConfig).toEqual({ enabled: false }); + }); + it('emits languageModelToolInvoked telemetry for each completed tool invocation', async () => { class RecordingTelemetryService extends NullTelemetryService { readonly events: { name: string; properties?: TelemetryEventProperties; measurements?: TelemetryEventMeasurements }[] = []; diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/permissionHelpers.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/permissionHelpers.spec.ts index 7c44c2db384f0..eec97b0af1442 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/permissionHelpers.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/permissionHelpers.spec.ts @@ -147,6 +147,21 @@ describe('CopilotCLI permissionHelpers', () => { // Bash regex doesn't recognize Set-Location, so full command is kept expect(result.input.command).toBe(fullCommandText); }); + + it('shell: passes sandboxBypass and reason when the command opts out of the sandbox', () => { + const req = { kind: 'shell', intention: 'Read secret', fullCommandText: 'cat ~/secret', requestSandboxBypass: true, requestSandboxBypassReason: 'Needs access outside the workspace' } as any; + const result = buildShellConfirmationParams(req, undefined); + expect(result.input.command).toBe('cat ~/secret'); + expect(result.input.sandboxBypass).toBe(true); + expect(result.input.sandboxBypassReason).toBe('Needs access outside the workspace'); + }); + + it('shell: omits sandboxBypass for ordinary commands', () => { + const req = { kind: 'shell', intention: 'List workspace files', fullCommandText: 'ls -la' } as any; + const result = buildShellConfirmationParams(req, undefined); + expect(result.input.sandboxBypass).toBeUndefined(); + expect(result.input.sandboxBypassReason).toBeUndefined(); + }); }); describe('buildMcpConfirmationParams', () => { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts index e9bdf3f1d9c07..f2fcb50255496 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts @@ -17,6 +17,7 @@ import { Disposable } from '../../../../util/vs/base/common/lifecycle'; import { basename } from '../../../../util/vs/base/common/resources'; import { URI } from '../../../../util/vs/base/common/uri'; import { ICopilotCLIAgents, isEnabledForCopilotCLI } from '../../copilotcli/node/copilotCli'; +import { INativeEnvService } from '../../../../platform/env/common/envService'; export class CopilotCLICustomizationProvider extends Disposable implements vscode.ChatSessionCustomizationProvider { @@ -44,6 +45,7 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod @ILogService private readonly logService: ILogService, @IWorkspaceService private readonly workspaceService: IWorkspaceService, @IFileSystemService private readonly fileSystemService: IFileSystemService, + @INativeEnvService private readonly envService: INativeEnvService, ) { super(); @@ -82,6 +84,32 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod return items; } + async provideSourceFolders(_sessionResource: vscode.Uri, type: vscode.ChatSessionCustomizationType, _token: vscode.CancellationToken): Promise { + const folders: vscode.ChatSessionCustomizationSourceFolder[] = []; + const roots = getSearchRoots(); + for (const folder of this.workspaceService.getWorkspaceFolders()) { + for (const root of roots.workspace) { + if (root.type === type) { + folders.push({ + uri: URI.joinPath(folder, ...root.path), + label: root.path[0], + source: 'local' + }); + } + } + } + for (const root of roots.user) { + if (root.type === type) { + folders.push({ + uri: URI.joinPath(this.envService.userHome, ...root.path), + label: `~/${root.path[0]}`, + source: 'user' + }); + } + } + return folders; + } + /** * Builds agent items from ICopilotCLIAgents, which already merges SDK * and prompt-file agents with source URIs. @@ -240,3 +268,25 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod })); } } + + +function getSearchRoots() { + return { + workspace: [ + { path: ['.github', 'agents'], type: vscode.ChatSessionCustomizationType.Agent }, + { path: ['.agents', 'agents'], type: vscode.ChatSessionCustomizationType.Agent }, + { path: ['.claude', 'agents'], type: vscode.ChatSessionCustomizationType.Agent }, + { path: ['.github', 'skills'], recursive: true, type: vscode.ChatSessionCustomizationType.Skill }, + { path: ['.agents', 'skills'], recursive: true, type: vscode.ChatSessionCustomizationType.Skill }, + { path: ['.claude', 'skills'], recursive: true, type: vscode.ChatSessionCustomizationType.Skill }, + { path: ['.github', 'instructions'], recursive: true, type: vscode.ChatSessionCustomizationType.Instructions }, + { path: ['.github', 'hooks'], recursive: true, type: vscode.ChatSessionCustomizationType.Hook }, + ], + user: [ + { path: ['.copilot', 'agents'], type: vscode.ChatSessionCustomizationType.Agent }, + { path: ['.agents', 'skills'], recursive: true, type: vscode.ChatSessionCustomizationType.Skill }, + { path: ['.copilot', 'instructions'], recursive: true, type: vscode.ChatSessionCustomizationType.Instructions }, + { path: ['.copilot', 'hooks'], recursive: true, type: vscode.ChatSessionCustomizationType.Hook }, + ], + }; +} diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLICustomizationProvider.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLICustomizationProvider.spec.ts index 26b7f5dfdb074..9d839270ef991 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLICustomizationProvider.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLICustomizationProvider.spec.ts @@ -126,6 +126,7 @@ describe('CopilotCLICustomizationProvider', () => { new TestLogService(), { getWorkspaceFolders: () => [] } as any, { stat: () => Promise.reject(new Error('not found')) } as any, + { userHome: URI.file('/home/test') } as any, )); }); @@ -347,6 +348,7 @@ describe('CopilotCLICustomizationProvider', () => { ? Promise.resolve({ type: 1, ctime: 0, mtime: 0, size: 0 }) : Promise.reject(new Error('not found')), } as any, + { userHome: URI.file('/home/test') } as any, )); mockPromptsService.setInstructions([]); diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLISDKUpgrade.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLISDKUpgrade.spec.ts index d3e0c4f38103a..55f5988b18e8d 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLISDKUpgrade.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLISDKUpgrade.spec.ts @@ -19,7 +19,7 @@ describe('CopilotCLI SDK Upgrade', function () { it('should be able to load the SDK without errors', async function () { await import('@github/copilot/sdk'); - }); + }, 10000); it('should not contain new native binaries nor removed native binaries', async function () { // This is a very basic check to ensure that when the Copilot CLI SDK is upgraded, @@ -27,35 +27,25 @@ describe('CopilotCLI SDK Upgrade', function () { // Such changes may require us to update our extension packaging or other handling. const existingBinaries = new Set(await findAllBinaries(copilotSDKPath)); const knownBinaries = new Set([ - // node-pty related files (already accounted for in SDK, using VS Code node-pty). - path.join('prebuilds', 'darwin-arm64', 'pty.node'), - path.join('prebuilds', 'darwin-x64', 'pty.node'), - path.join('prebuilds', 'linux-arm64', 'pty.node'), - path.join('prebuilds', 'linux-x64', 'pty.node'), - path.join('prebuilds', 'win32-arm64', 'conpty', 'OpenConsole.exe'), - path.join('prebuilds', 'win32-arm64', 'conpty', 'conpty.dll'), - path.join('prebuilds', 'win32-arm64', 'conpty.node'), - path.join('prebuilds', 'win32-arm64', 'conpty.pdb'), - path.join('prebuilds', 'win32-arm64', 'conpty_console_list.node'), - path.join('prebuilds', 'win32-arm64', 'conpty_console_list.pdb'), - path.join('prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe'), - path.join('prebuilds', 'win32-x64', 'conpty', 'conpty.dll'), - path.join('prebuilds', 'win32-x64', 'conpty.node'), - path.join('prebuilds', 'win32-x64', 'conpty.pdb'), - path.join('prebuilds', 'win32-x64', 'conpty_console_list.node'), - path.join('prebuilds', 'win32-x64', 'conpty_console_list.pdb'), // ripgrep path.join('ripgrep', 'bin', 'win32-arm64', 'rg.exe'), path.join('ripgrep', 'bin', 'win32-x64', 'rg.exe'), - path.join('prebuilds', 'darwin-arm64', 'spawn-helper'), - path.join('prebuilds', 'darwin-x64', 'spawn-helper'), - // computer use - path.join('prebuilds', 'darwin-arm64', 'computer.node'), - path.join('prebuilds', 'darwin-x64', 'computer.node'), - path.join('prebuilds', 'linux-arm64', 'computer.node'), - path.join('prebuilds', 'linux-x64', 'computer.node'), - path.join('prebuilds', 'win32-arm64', 'computer.node'), - path.join('prebuilds', 'win32-x64', 'computer.node'), + // Computer-use payloads present in the raw package. Root prebuilds + // are stripped from the shipped extension by .vscodeignore. + path.join('prebuilds', 'darwin-arm64', 'computer-use-mcp'), + path.join('prebuilds', 'darwin-arm64', 'Copilot Computer Use.app', 'Contents', 'CodeResources'), + path.join('prebuilds', 'darwin-arm64', 'Copilot Computer Use.app', 'Contents', 'Resources', 'Assets.car'), + path.join('prebuilds', 'darwin-arm64', 'Copilot Computer Use.app', 'Contents', 'Resources', 'icon.icns'), + path.join('prebuilds', 'darwin-arm64', 'Copilot Computer Use.app', 'Contents', 'MacOS', 'Copilot Computer Use'), + path.join('prebuilds', 'darwin-x64', 'computer-use-mcp'), + path.join('prebuilds', 'darwin-x64', 'Copilot Computer Use.app', 'Contents', 'CodeResources'), + path.join('prebuilds', 'darwin-x64', 'Copilot Computer Use.app', 'Contents', 'Resources', 'Assets.car'), + path.join('prebuilds', 'darwin-x64', 'Copilot Computer Use.app', 'Contents', 'Resources', 'icon.icns'), + path.join('prebuilds', 'darwin-x64', 'Copilot Computer Use.app', 'Contents', 'MacOS', 'Copilot Computer Use'), + path.join('prebuilds', 'win32-arm64', 'CopilotComputerUse.exe'), + path.join('prebuilds', 'win32-arm64', 'computer-use-mcp.exe'), + path.join('prebuilds', 'win32-x64', 'CopilotComputerUse.exe'), + path.join('prebuilds', 'win32-x64', 'computer-use-mcp.exe'), // cli-native to be included path.join('prebuilds', 'darwin-arm64', 'cli-native.node'), path.join('prebuilds', 'darwin-x64', 'cli-native.node'), @@ -77,34 +67,14 @@ describe('CopilotCLI SDK Upgrade', function () { path.join('prebuilds', 'win32-x64', 'runtime.node'), // Second copy of native prebuilds re-shipped by the @github/copilot/sdk subpackage // (previously hidden by a broad sdk/prebuilds/** exclusion that masked the node-pty files we used to shim in at test setup). - path.join('sdk', 'prebuilds', 'darwin-arm64', 'computer.node'), - path.join('sdk', 'prebuilds', 'darwin-x64', 'computer.node'), - path.join('sdk', 'prebuilds', 'linux-arm64', 'computer.node'), - path.join('sdk', 'prebuilds', 'linux-x64', 'computer.node'), - path.join('sdk', 'prebuilds', 'win32-arm64', 'computer.node'), - path.join('sdk', 'prebuilds', 'win32-x64', 'computer.node'), path.join('sdk', 'prebuilds', 'darwin-arm64', 'runtime.node'), path.join('sdk', 'prebuilds', 'darwin-x64', 'runtime.node'), path.join('sdk', 'prebuilds', 'linux-arm64', 'runtime.node'), path.join('sdk', 'prebuilds', 'linux-x64', 'runtime.node'), + path.join('sdk', 'prebuilds', 'linuxmusl-arm64', 'runtime.node'), + path.join('sdk', 'prebuilds', 'linuxmusl-x64', 'runtime.node'), path.join('sdk', 'prebuilds', 'win32-arm64', 'runtime.node'), path.join('sdk', 'prebuilds', 'win32-x64', 'runtime.node'), - // node-pty natives re-shipped into the @github/copilot/sdk subpackage by our - // postinstall so built-in installs can spawn through node-pty (used by mxc). - path.join('sdk', 'prebuilds', 'darwin-arm64', 'pty.node'), - path.join('sdk', 'prebuilds', 'darwin-x64', 'pty.node'), - path.join('sdk', 'prebuilds', 'linux-arm64', 'pty.node'), - path.join('sdk', 'prebuilds', 'linux-x64', 'pty.node'), - path.join('sdk', 'prebuilds', 'darwin-arm64', 'spawn-helper'), - path.join('sdk', 'prebuilds', 'darwin-x64', 'spawn-helper'), - path.join('sdk', 'prebuilds', 'win32-arm64', 'conpty.node'), - path.join('sdk', 'prebuilds', 'win32-x64', 'conpty.node'), - path.join('sdk', 'prebuilds', 'win32-arm64', 'conpty_console_list.node'), - path.join('sdk', 'prebuilds', 'win32-x64', 'conpty_console_list.node'), - path.join('sdk', 'prebuilds', 'win32-arm64', 'conpty', 'OpenConsole.exe'), - path.join('sdk', 'prebuilds', 'win32-arm64', 'conpty', 'conpty.dll'), - path.join('sdk', 'prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe'), - path.join('sdk', 'prebuilds', 'win32-x64', 'conpty', 'conpty.dll'), path.join('ripgrep', 'bin', 'darwin-arm64', 'rg'), path.join('ripgrep', 'bin', 'darwin-x64', 'rg'), path.join('ripgrep', 'bin', 'linux-x64', 'rg'), @@ -134,6 +104,8 @@ describe('CopilotCLI SDK Upgrade', function () { path.join('sdk', 'prebuilds', 'darwin-x64', 'cli-native.node'), path.join('sdk', 'prebuilds', 'linux-arm64', 'cli-native.node'), path.join('sdk', 'prebuilds', 'linux-x64', 'cli-native.node'), + path.join('sdk', 'prebuilds', 'linuxmusl-arm64', 'cli-native.node'), + path.join('sdk', 'prebuilds', 'linuxmusl-x64', 'cli-native.node'), path.join('sdk', 'prebuilds', 'win32-arm64', 'cli-native.node'), path.join('sdk', 'prebuilds', 'win32-x64', 'cli-native.node'), // foundry-local-sdk vendored native bindings. @@ -148,34 +120,6 @@ describe('CopilotCLI SDK Upgrade', function () { path.join('pvrecorder', 'node_modules', '@picovoice', 'pvrecorder-node', 'lib', 'mac', 'x86_64', 'pv_recorder.node'), path.join('pvrecorder', 'node_modules', '@picovoice', 'pvrecorder-node', 'lib', 'windows', 'amd64', 'pv_recorder.node'), path.join('pvrecorder', 'node_modules', '@picovoice', 'pvrecorder-node', 'lib', 'windows', 'arm64', 'pv_recorder.node'), - // mxc-bin (Windows sandbox + WSL helpers used by the SDK's command execution). - path.join('mxc-bin', 'arm64', 'lxc-exec'), - path.join('mxc-bin', 'arm64', 'mxc-exec-mac'), - path.join('mxc-bin', 'arm64', 'winhttp-proxy-shim.exe'), - path.join('mxc-bin', 'arm64', 'wslcsdk.dll'), - path.join('mxc-bin', 'arm64', 'wxc-exec.exe'), - path.join('mxc-bin', 'arm64', 'wxc-test-proxy.exe'), - path.join('mxc-bin', 'arm64', 'wxc-host-prep.exe'), - path.join('mxc-bin', 'arm64', 'wxc-windows-sandbox-daemon.exe'), - path.join('mxc-bin', 'arm64', 'wxc-windows-sandbox-guest.exe'), - path.join('mxc-bin', 'arm64', 'mxc-diagnostic-console.exe'), - path.join('mxc-bin', 'arm64', '_manifest', 'spdx_2.2', 'bsi.cose'), - path.join('mxc-bin', 'arm64', '_manifest', 'spdx_2.2', 'manifest.cat'), - path.join('mxc-bin', 'arm64', '_manifest', 'spdx_2.2', 'manifest.spdx.cose'), - path.join('mxc-bin', 'arm64', 'linux-test-proxy'), - path.join('mxc-bin', 'x64', 'lxc-exec'), - path.join('mxc-bin', 'x64', 'winhttp-proxy-shim.exe'), - path.join('mxc-bin', 'x64', 'wslcsdk.dll'), - path.join('mxc-bin', 'x64', 'wxc-exec.exe'), - path.join('mxc-bin', 'x64', 'wxc-test-proxy.exe'), - path.join('mxc-bin', 'x64', 'wxc-host-prep.exe'), - path.join('mxc-bin', 'x64', 'wxc-windows-sandbox-daemon.exe'), - path.join('mxc-bin', 'x64', 'wxc-windows-sandbox-guest.exe'), - path.join('mxc-bin', 'x64', 'mxc-diagnostic-console.exe'), - path.join('mxc-bin', 'x64', '_manifest', 'spdx_2.2', 'bsi.cose'), - path.join('mxc-bin', 'x64', '_manifest', 'spdx_2.2', 'manifest.cat'), - path.join('mxc-bin', 'x64', '_manifest', 'spdx_2.2', 'manifest.spdx.cose'), - path.join('mxc-bin', 'x64', 'linux-test-proxy'), // parsing commands for shell. 'tree-sitter-bash.wasm', 'tree-sitter.wasm', @@ -197,10 +141,6 @@ describe('CopilotCLI SDK Upgrade', function () { 'tree-sitter-scala.wasm', ].map(p => path.join(copilotSDKPath, p))); - const optionalKnownBinaries = new Set([ - path.join(copilotSDKPath, 'mxc-bin', 'x64', 'mxc-exec-mac'), - ]); - // Exclude ripgrep files that we copy over in src/extension/chatSessions/copilotcli/node/ripgrepShim.ts (until we get better API/solution from SDK) const ripgrepFilesWeCopy = path.join(copilotSDKPath, 'sdk', 'ripgrep', 'bin'); @@ -214,7 +154,7 @@ describe('CopilotCLI SDK Upgrade', function () { if (binaryName.startsWith('keytar') || binaryName.startsWith('clipboard')) { continue; } - if (!knownBinaries.has(binary) && !optionalKnownBinaries.has(binary)) { + if (!knownBinaries.has(binary)) { errors.push(`Unexpected native binary found in Copilot CLI SDK: ${path.relative(copilotSDKPath, binary)}`); } } @@ -224,6 +164,9 @@ describe('CopilotCLI SDK Upgrade', function () { continue; } if (!existingBinaries.has(binary)) { + if (isNonCurrentCopilotPlatformBinary(copilotSDKPath, binary) || isNonCurrentPvRecorderBinary(copilotSDKPath, binary)) { + continue; + } errors.push(`Expected native binary missing from Copilot CLI SDK: ${path.relative(copilotSDKPath, binary)}`); } } @@ -231,13 +174,66 @@ describe('CopilotCLI SDK Upgrade', function () { if (errors.length > 0) { throw new Error(errors.join('\n')); } - }); + }, 30000); it('should be able to load the @github/copilot module without errors', async function () { await import('@github/copilot/sdk'); }); }); +const copilotPlatformArchs = new Set([ + 'darwin-arm64', + 'darwin-x64', + 'linux-arm64', + 'linux-x64', + 'linuxmusl-arm64', + 'linuxmusl-x64', + 'win32-arm64', + 'win32-x64', +]); + +function currentCopilotPlatformArch(): string { + const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined; + if (process.platform === 'linux' && !report?.header?.glibcVersionRuntime) { + return `linuxmusl-${process.arch}`; + } + + return `${process.platform}-${process.arch}`; +} + +function isNonCurrentCopilotPlatformBinary(copilotSDKPath: string, binary: string): boolean { + const relativeSegments = path.relative(copilotSDKPath, binary).split(path.sep); + const platformArch = relativeSegments.find(segment => copilotPlatformArchs.has(segment)); + return platformArch !== undefined && platformArch !== currentCopilotPlatformArch(); +} + +function isNonCurrentPvRecorderBinary(copilotSDKPath: string, binary: string): boolean { + const relative = path.relative(copilotSDKPath, binary).split(path.sep).join(path.posix.sep); + const pvRecorderPrefix = 'pvrecorder/node_modules/@picovoice/pvrecorder-node/lib/'; + if (!relative.startsWith(pvRecorderPrefix)) { + return false; + } + + const currentPlatformArch = currentCopilotPlatformArch(); + if (relative.startsWith(`${pvRecorderPrefix}mac/arm64/`)) { + return currentPlatformArch !== 'darwin-arm64'; + } + if (relative.startsWith(`${pvRecorderPrefix}mac/x86_64/`)) { + return currentPlatformArch !== 'darwin-x64'; + } + if (relative.startsWith(`${pvRecorderPrefix}linux/x86_64/`)) { + return currentPlatformArch !== 'linux-x64' && currentPlatformArch !== 'linuxmusl-x64'; + } + if (relative.startsWith(`${pvRecorderPrefix}windows/amd64/`)) { + return currentPlatformArch !== 'win32-x64'; + } + if (relative.startsWith(`${pvRecorderPrefix}windows/arm64/`)) { + return currentPlatformArch !== 'win32-arm64'; + } + + return false; +} + async function copyBinaries(extensionPath: string) { const copilotSDKPath = path.join(extensionPath, 'node_modules', '@github', 'copilot'); const vscodeRipgrepPath = path.join(copilotSDKPath, 'ripgrep', 'bin', process.platform + '-' + process.arch); diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts index 1eb0924b21a58..e1f9222abbee4 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts @@ -169,6 +169,32 @@ export class ClaudeCustomizationProvider extends Disposable implements vscode.Ch return items; } + async provideSourceFolders(_sessionResource: vscode.Uri, type: vscode.ChatSessionCustomizationType, _token: vscode.CancellationToken): Promise { + const folders: vscode.ChatSessionCustomizationSourceFolder[] = []; + const searchPaths = getCustomizationSearchPaths(); + for (const folder of this.workspaceService.getWorkspaceFolders()) { + for (const root of searchPaths.workspace) { + if (root.type === type) { + folders.push({ + uri: URI.joinPath(folder, ...root.path), + label: root.path[0], + source: 'local' + }); + } + } + } + for (const root of searchPaths.user) { + if (root.type === type) { + folders.push({ + uri: URI.joinPath(this.envService.userHome, ...root.path), + label: `~/${root.path[0]}`, + source: 'user' + }); + } + } + return folders; + } + private async discoverInstructions(): Promise { const items: vscode.ChatSessionCustomizationItem[] = []; const candidates: { uri: URI; source: vscode.ChatResourceSource }[] = []; @@ -296,3 +322,17 @@ export function isEnabledForClaudeCode(customization: { sessionTypes?: readonly const sessionTypes = customization.sessionTypes; return sessionTypes === undefined || sessionTypes.includes('claude-code') || false; } + +function getCustomizationSearchPaths() { + return { + workspace: [ + { path: ['.claude', 'agents'], type: vscode.ChatSessionCustomizationType.Agent }, + { path: ['.claude', 'skills'], recursive: true, type: vscode.ChatSessionCustomizationType.Skill }, + ], + user: [ + { path: ['.claude', 'agents'], type: vscode.ChatSessionCustomizationType.Agent }, + { path: ['.agents', 'skills'], recursive: true, type: vscode.ChatSessionCustomizationType.Skill }, + { path: ['.copilot', 'instructions'], recursive: true, type: vscode.ChatSessionCustomizationType.Instructions }, + ], + }; +} diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts index a48452fd9fc3d..9735cb2de182a 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts @@ -5,7 +5,7 @@ import * as pathLib from 'path'; import * as vscode from 'vscode'; -import type { AgentTaskSessionEvent } from '@vscode/copilot-api'; +import type { AgentTaskSessionEvent, AgentTaskState } from '@vscode/copilot-api'; import { l10n, Uri } from 'vscode'; import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; @@ -150,6 +150,36 @@ export function parseSessionLogChunksSafely(rawText: string, logService: ILogSer } } +/** + * Map a Task API lifecycle state (v2) directly to a {@link vscode.ChatSessionStatus}. Unlike v1's + * PR-derived status (which routes through the lossy `SessionInfo['state']`), this keeps the + * non-terminal "agent handed the turn back" states distinct from active work: `idle` (agent + * finished its turn, nothing pending) renders as Completed and `waiting_for_user` (agent paused for + * input) renders as NeedsInput. Only `queued`/`in_progress` are genuinely InProgress — collapsing + * `idle`/`waiting_for_user` into InProgress made settled tasks look like they were still running. + */ +export function taskStateToChatSessionStatus(state: AgentTaskState): vscode.ChatSessionStatus { + switch (state) { + case 'queued': + case 'in_progress': + return vscode.ChatSessionStatus.InProgress; + case 'waiting_for_user': + return vscode.ChatSessionStatus.NeedsInput; + case 'idle': + case 'completed': + return vscode.ChatSessionStatus.Completed; + case 'failed': + case 'timed_out': + case 'cancelled': + return vscode.ChatSessionStatus.Failed; + default: + // Forward-compat fallback: a state outside the known union (e.g. a state added + // server-side after this build) must not yield an invalid `undefined` status. Treat + // unknowns as InProgress — the conservative "agent may still own the turn" assumption. + return vscode.ChatSessionStatus.InProgress; + } +} + const CUSTOM_AGENTS_OPTION_GROUP_ID = 'customAgents'; const MODELS_OPTION_GROUP_ID = 'models'; const PARTNER_AGENTS_OPTION_GROUP_ID = 'partnerAgents'; @@ -1272,6 +1302,13 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C const sessionItem = entry.latestSession; const createdAt = validateISOTimestamp(sessionItem.created_at); + // Task-backed entries (v2) carry their raw lifecycle state; map it directly so the + // agent's post-turn states (`idle`/`waiting_for_user`) don't render as InProgress. + // Jobs API (v1) entries fall back to the PR-derived session state. + const status = entry.taskState !== undefined + ? taskStateToChatSessionStatus(entry.taskState) + : this.getSessionStatusFromSession(sessionItem); + // Task-shaped card path (v2 with no resolvable PR yet, or PR-less by design). if (!pr) { if (!entry.pullArtifact && entry.latestSession.resource_type !== 'task') { @@ -1288,7 +1325,7 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C return { resource: vscode.Uri.from({ scheme: CopilotCloudSessionsProvider.TYPE, path: '/' + SessionIdForTask.getId(taskId) }), label: entry.latestSession.name || taskId, - status: this.getSessionStatusFromSession(sessionItem), + status, ...(changes?.length ? { changes } : {}), ...(createdAt ? { timing: { @@ -1327,7 +1364,7 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C const session = { resource, label: pr.title, - status: this.getSessionStatusFromSession(sessionItem), + status, badge: this.getPullRequestBadge(repoIds, pr), tooltip: this.createPullRequestTooltip(pr), ...(createdAt ? { diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/taskApiBackend.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/taskApiBackend.ts index 96a7d58b6baf9..457660e220f3e 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/taskApiBackend.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/taskApiBackend.ts @@ -64,6 +64,36 @@ function mapTaskStateToSessionState(state: AgentTaskState): SessionInfo['state'] } } +/** + * Agent integration slugs that identify the Copilot cloud coding agent. CMC/CAPI returns + * `copilot-developer`; the monolith uses `copilot-swe-agent` for the same agent. Tasks owned by + * any other surface — `copilot-developer-cli` (Copilot CLI), `vscode-chat` (VS Code) or + * `jetbrains-chat` (JetBrains) — are local clients mirrored into Mission Control and must not + * appear in the cloud sessions list. See github-ui `agent-helpers.ts` (`isCopilotCodingAgent`) and + * `agent-profile.ts`. + */ +const CLOUD_CODING_AGENT_SLUGS: ReadonlySet = new Set(['copilot-developer', 'copilot-swe-agent']); + +/** + * The owning agent integration of a task. Mirrors CMC's internal `TaskCollaborator` + * (`agent_collaborators`), which first-party CAPI tokens receive but `@vscode/copilot-api`'s + * `AgentTask` does not yet model. Only `slug` is needed to identify the client surface. + */ +interface TaskAgentCollaborator { + readonly slug?: string; +} + +/** + * Whether a task is owned by the Copilot cloud coding agent rather than a local client surface + * (Copilot CLI / VS Code / JetBrains). The owning surface is identified by the agent integration + * slug on the task's `agent_collaborators`; tasks without a recognized cloud slug are treated as + * non-cloud and excluded from the cloud sessions list. + */ +export function isCloudCodingAgentTask(task: AgentTask): boolean { + const collaborators = (task as AgentTask & { readonly agent_collaborators?: readonly TaskAgentCollaborator[] }).agent_collaborators; + return collaborators?.some(c => typeof c.slug === 'string' && CLOUD_CODING_AGENT_SLUGS.has(c.slug)) ?? false; +} + function findPullArtifact(task: AgentTask): (AgentTaskArtifact & { data: AgentTaskGitHubResourceData }) | undefined { return task.artifacts?.find( (a): a is AgentTaskArtifact & { data: AgentTaskGitHubResourceData } => @@ -300,11 +330,12 @@ export class TaskApiBackend implements TaskCloudAgentBackend { } return tasksWithRepo - .filter(({ task }) => !task.archived_at) + .filter(({ task }) => !task.archived_at && isCloudCodingAgentTask(task)) .map(({ task, repo }): CloudSessionData => ({ latestSession: taskToSessionInfo(task), pullArtifact: taskToPullArtifactRef(task, repo), diffRefs: taskToDiffRefs(task, repo), + taskState: task.state, })); } @@ -475,8 +506,6 @@ export class TaskApiError extends Error { */ export class TaskApiHttpClient implements ITaskApiClient { - private static readonly SIGN_IN_DETAIL = l10n.t('Sign in to GitHub to use the Cloud Agent (Task API).'); - constructor( private readonly _capiClientService: ICAPIClientService, private readonly _authService: IAuthenticationService, @@ -484,8 +513,7 @@ export class TaskApiHttpClient implements ITaskApiClient { ) { } private async _authHeaders(): Promise> { - const session = await this._authService.getGitHubSession('permissive', { silent: true }) - ?? await this._authService.getGitHubSession('permissive', { createIfNone: { detail: TaskApiHttpClient.SIGN_IN_DETAIL } }); + const session = await this._authService.getGitHubSession('permissive', { silent: true }); const token = session?.accessToken; if (!token) { throw new Error(l10n.t('Sign in to GitHub to use the Cloud Agent.')); diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts index 577dbf2a4b5d3..630240433ff1f 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts @@ -422,7 +422,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { } }(); } - const session = new TestCopilotCLISession(workspaceInfo, agentName, sdkSession, [], false, logService, workspaceService, new MockChatSessionMetadataStore(), instantiationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService(), new FakeUserQuestionHandler(), accessor.get(IConfigurationService), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new FakeGitService(), { _serviceBrand: undefined } as any, { _serviceBrand: undefined, resetTurnCredits() { }, getCreditsForTurn() { return undefined; }, setLastCopilotUsage() { } } as any, new NullTelemetryService()); + const session = new TestCopilotCLISession(workspaceInfo, agentName, sdkSession, [], undefined, logService, workspaceService, new MockChatSessionMetadataStore(), instantiationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService(), new FakeUserQuestionHandler(), accessor.get(IConfigurationService), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new FakeGitService(), { _serviceBrand: undefined } as any, { _serviceBrand: undefined, resetTurnCredits() { }, getCreditsForTurn() { return undefined; }, setLastCopilotUsage() { } } as any, new NullTelemetryService()); cliSessions.push(session); return disposables.add(session); } diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCloudSessionsProvider.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCloudSessionsProvider.spec.ts index aa3094718197a..0dcd0ec45c5dd 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCloudSessionsProvider.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCloudSessionsProvider.spec.ts @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as vscode from 'vscode'; -import type { AgentTask, AgentTaskCreateRequest, AgentTaskGetResponse, AgentTaskListEventsResponse, AgentTaskListResponse, AgentTaskSessionEvent, AgentTaskSteerRequest, AgentTaskCreatePullRequestResponse } from '@vscode/copilot-api'; +import type { AgentTask, AgentTaskCreateRequest, AgentTaskGetResponse, AgentTaskListEventsResponse, AgentTaskListResponse, AgentTaskSessionEvent, AgentTaskState, AgentTaskSteerRequest, AgentTaskCreatePullRequestResponse } from '@vscode/copilot-api'; import { GithubRepoId, IGitService } from '../../../../platform/git/common/gitService'; import { PullRequestSearchItem, SessionInfo } from '../../../../platform/github/common/githubAPI'; import { TestLogService } from '../../../../platform/testing/common/testLogService'; @@ -13,8 +13,8 @@ import { mock } from '../../../../util/common/test/simpleMock'; import { ChatRequestTurn2, ChatResponseMarkdownPart, ChatResponseTurn2, ChatToolInvocationPart } from '../../../../vscodeTypes'; import { ITaskApiClient, ListTaskEventsOptions, ListTasksOptions } from '../../common/taskApiTypes'; import { ChatSessionContentBuilder } from '../copilotCloudSessionContentBuilder'; -import { normalizeInitialSessionOptions, parseSessionLogChunksSafely } from '../copilotCloudSessionsProvider'; -import { TaskApiBackend, parseRepoFromTaskUrl } from '../taskApiBackend'; +import { normalizeInitialSessionOptions, parseSessionLogChunksSafely, taskStateToChatSessionStatus } from '../copilotCloudSessionsProvider'; +import { TaskApiBackend, parseRepoFromTaskUrl, isCloudCodingAgentTask } from '../taskApiBackend'; import { NullCloudBackendInstrumentation } from '../cloudBackendTelemetry'; import { MockOctoKitService } from '../../../agents/vscode-node/test/mockOctoKitService'; @@ -457,6 +457,92 @@ describe('TaskApiBackend', () => { expect(client.listForRepoCalls).toEqual([]); expect(client.listCalls).toEqual([{ options: { per_page: 100 } }]); }); + + it('fetchSessionList carries the raw task lifecycle state so settled tasks are not shown as in_progress', async () => { + // `idle` collapses to `in_progress` in the legacy SessionInfo shape; the raw `taskState` + // must be preserved alongside it so the provider can render it as Completed/NeedsInput. + const client = new FakeTaskApiClient({ repoTasks: [{ id: 't-idle', state: 'idle', created_at: '2026-03-27T00:00:00Z', creator: { id: 4242 }, agent_collaborators: [{ slug: 'copilot-developer' }] } as unknown as AgentTask] }); + const octoKitService = new MockOctoKitService(); + octoKitService.getCurrentAuthedUser = async () => ({ id: 4242, login: 'octocat', name: 'The Octocat', avatar_url: '' }); + const backend = new TaskApiBackend(client, new TestLogService(), octoKitService, NullCloudBackendInstrumentation); + + const result = await backend.fetchSessionList([new GithubRepoId('octocat', 'hello-world')], false, false); + + expect(result.map(r => ({ taskState: r.taskState, sessionState: r.latestSession.state }))).toEqual([ + { taskState: 'idle', sessionState: 'in_progress' }, + ]); + }); + + it('fetchSessionList shows only cloud coding agent tasks and excludes local-client tasks (CLI / VS Code / JetBrains)', async () => { + const repoTasks = [ + { id: 'cloud-dev', state: 'idle', created_at: '2026-03-27T00:00:00Z', creator: { id: 4242 }, agent_collaborators: [{ slug: 'copilot-developer' }] }, + { id: 'cloud-swe', state: 'idle', created_at: '2026-03-27T00:00:00Z', creator: { id: 4242 }, agent_collaborators: [{ slug: 'copilot-swe-agent' }] }, + { id: 'cli', state: 'idle', created_at: '2026-03-27T00:00:00Z', creator: { id: 4242 }, agent_collaborators: [{ slug: 'copilot-developer-cli' }] }, + { id: 'vscode', state: 'idle', created_at: '2026-03-27T00:00:00Z', creator: { id: 4242 }, agent_collaborators: [{ slug: 'vscode-chat' }] }, + { id: 'jetbrains', state: 'idle', created_at: '2026-03-27T00:00:00Z', creator: { id: 4242 }, agent_collaborators: [{ slug: 'jetbrains-chat' }] }, + { id: 'no-collaborators', state: 'idle', created_at: '2026-03-27T00:00:00Z', creator: { id: 4242 } }, + ] as unknown as readonly AgentTask[]; + const client = new FakeTaskApiClient({ repoTasks }); + const octoKitService = new MockOctoKitService(); + octoKitService.getCurrentAuthedUser = async () => ({ id: 4242, login: 'octocat', name: 'The Octocat', avatar_url: '' }); + const backend = new TaskApiBackend(client, new TestLogService(), octoKitService, NullCloudBackendInstrumentation); + + const result = await backend.fetchSessionList([new GithubRepoId('octocat', 'hello-world')], false, false); + + expect(result.map(r => r.latestSession.id)).toEqual(['cloud-dev', 'cloud-swe']); + }); +}); + +describe('isCloudCodingAgentTask', () => { + it('keeps cloud coding agent slugs and rejects local-client / missing / malformed slugs', () => { + const classify = (agent_collaborators?: Array<{ slug?: unknown }>) => + isCloudCodingAgentTask({ id: 't', state: 'idle', created_at: '2026-03-27T00:00:00Z', ...(agent_collaborators && { agent_collaborators }) } as unknown as AgentTask); + + expect({ + 'copilot-developer': classify([{ slug: 'copilot-developer' }]), + 'copilot-swe-agent': classify([{ slug: 'copilot-swe-agent' }]), + 'copilot-developer-cli': classify([{ slug: 'copilot-developer-cli' }]), + 'vscode-chat': classify([{ slug: 'vscode-chat' }]), + 'jetbrains-chat': classify([{ slug: 'jetbrains-chat' }]), + 'missing-slug': classify([{}]), + 'null-slug': classify([{ slug: null }]), + 'no-collaborators': classify(undefined), + 'empty-collaborators': classify([]), + }).toEqual({ + 'copilot-developer': true, + 'copilot-swe-agent': true, + 'copilot-developer-cli': false, + 'vscode-chat': false, + 'jetbrains-chat': false, + 'missing-slug': false, + 'null-slug': false, + 'no-collaborators': false, + 'empty-collaborators': false, + }); + }); +}); + +describe('taskStateToChatSessionStatus', () => { + it('maps each Task API lifecycle state to the right ChatSessionStatus', () => { + const states: readonly AgentTaskState[] = ['queued', 'in_progress', 'idle', 'waiting_for_user', 'completed', 'failed', 'timed_out', 'cancelled']; + const mapped = Object.fromEntries(states.map(state => [state, taskStateToChatSessionStatus(state)])); + + expect(mapped).toEqual({ + queued: vscode.ChatSessionStatus.InProgress, + in_progress: vscode.ChatSessionStatus.InProgress, + // Agent finished its turn / is waiting — must not look like active work. + idle: vscode.ChatSessionStatus.Completed, + waiting_for_user: vscode.ChatSessionStatus.NeedsInput, + completed: vscode.ChatSessionStatus.Completed, + failed: vscode.ChatSessionStatus.Failed, + timed_out: vscode.ChatSessionStatus.Failed, + cancelled: vscode.ChatSessionStatus.Failed, + }); + }); + + it('falls back to InProgress for an unknown/forward-compat state instead of returning undefined', () => { + expect(taskStateToChatSessionStatus('some_new_server_state' as AgentTaskState)).toBe(vscode.ChatSessionStatus.InProgress); + }); }); describe('parseRepoFromTaskUrl', () => { diff --git a/extensions/copilot/src/extension/chatSessions/vscode/cloudAgentBackend.ts b/extensions/copilot/src/extension/chatSessions/vscode/cloudAgentBackend.ts index d697126dced80..eb26e1d244c41 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode/cloudAgentBackend.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode/cloudAgentBackend.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { AgentTaskCreatePullRequestResponse, AgentTaskGetResponse, AgentTaskSessionEvent } from '@vscode/copilot-api'; +import { AgentTaskCreatePullRequestResponse, AgentTaskGetResponse, AgentTaskSessionEvent, AgentTaskState } from '@vscode/copilot-api'; import { GithubRepoId } from '../../../platform/git/common/gitService'; import { PullRequestSearchItem, SessionInfo } from '../../../platform/github/common/githubAPI'; @@ -90,6 +90,14 @@ export interface CloudSessionData { * (changes come from the PR) and for tasks with no branch. */ readonly diffRefs?: { readonly owner: string; readonly repo: string; readonly baseRef: string; readonly headRef: string }; + + /** + * Raw Task API lifecycle state for Task-backed entries (v2). The provider maps this directly + * to a `ChatSessionStatus` so it can keep the non-terminal "agent handed the turn back" states + * (`idle`, `waiting_for_user`) distinct from active work — they must not render as InProgress. + * Absent for Jobs API (v1) entries, whose status is derived from `latestSession.state`. + */ + readonly taskState?: AgentTaskState; } /** diff --git a/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts index ce5823f104e2a..80f6d98b4954e 100644 --- a/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts @@ -144,6 +144,64 @@ export function getModelCapabilitiesDescription(endpoint: IChatEndpoint | Langua return undefined; } +/** + * Documentation link surfaced in the Auto model description. + * NOTE: Also defined in src/vs/workbench/contrib/chat/common/languageModels.ts (ILanguageModelChatMetadata.autoModelSelectionDocsUrl) — keep in sync. + */ +const AUTO_MODEL_DOCS_URL = 'https://docs.github.com/en/copilot/concepts/models/auto-model-selection'; + +/** + * Classifies an Auto discount range (given as fractions, e.g. `0.1` for 10%) + * into whole-number percentages. Returns `undefined` when there is no discount + * to show, `{ low }` for a single value, or `{ low, high }` for a range. + */ +function classifyDiscountRange(discountRange?: { low: number; high: number }): { low: number; high?: number } | undefined { + if (!discountRange) { + return undefined; + } + const low = Math.round(discountRange.low * 100); + const high = Math.round(discountRange.high * 100); + if (low === high) { + return low !== 0 ? { low } : undefined; + } + return { low, high }; +} + +/** + * Formats the Auto discount as a short label (e.g. "10% discount" or + * "10% to 20% discount"). Returns `undefined` when there is no discount. + * + * @param discountRange Discount as fractions (e.g. `0.1` for 10%). + */ +export function getAutoModelDiscountLabel(discountRange?: { low: number; high: number }): string | undefined { + const discount = classifyDiscountRange(discountRange); + if (!discount) { + return undefined; + } + return discount.high === undefined + ? l10n.t('{0}% discount', discount.low) + : l10n.t('{0}% to {1}% discount', discount.low, discount.high); +} + +/** + * Builds the shared description shown for the Auto model. The discount sentence + * is only included when a non-zero discount is provided. + * + * @param discountRange Discount as fractions (e.g. `0.1` for 10%). When omitted + * or zero, the discount sentence is left out entirely. + */ +export function getAutoModelDescription(discountRange?: { low: number; high: number }): string { + const base = l10n.t('Auto routes based on your task and real-time system health and model performance.'); + const learnMore = l10n.t('[Learn More]({0})', AUTO_MODEL_DOCS_URL); + const discount = classifyDiscountRange(discountRange); + const discountSentence = !discount + ? undefined + : discount.high === undefined + ? l10n.t('Models routed via auto receive a {0}% discount.', discount.low) + : l10n.t('Models routed via auto receive a {0}% to {1}% discount.', discount.low, discount.high); + return discountSentence ? `${base} ${discountSentence} ${learnMore}` : `${base} ${learnMore}`; +} + function formatAicPrice(price: number): string { if (price === 0) { return '0'; @@ -184,12 +242,19 @@ export function formatPricingLabel(pricing: IChatEndpointTokenPricing): string { } const TOKENS_PER_MILLION = 1_000_000; +const NANO_AIU_DIVISOR = 1_000_000_000; /** - * Raw token prices from the CAPI billing response (API 2026-06-01+, prices in AIUs). + * Raw token prices from the CAPI billing response. Supports both the tiered + * format (API 2026-06-01+, prices in AIUs) and the legacy flat format + * (pre-2026-06-01, prices in nano-AIUs) which is still used by some endpoints + * such as the cloud agents (`/agents/swe/models`) model picker. */ export interface IRawTokenPrices { batch_size?: number; + input_price?: number; + cache_price?: number; + output_price?: number; default?: { input_price?: number; cache_price?: number; cache_write_price?: number; output_price?: number; context_max?: number }; long_context?: { input_price?: number; cache_price?: number; cache_write_price?: number; output_price?: number; context_max?: number }; } @@ -209,6 +274,7 @@ export interface INormalizedTokenPricing { /** * Converts raw billing token prices into normalized AICs (credits) per million tokens. + * Handles both tiered (AIU) and legacy flat (nano-AIU) formats. * * When a `long_context` tier is present but its prices match the `default` tier, * it is omitted from the result. @@ -251,6 +317,16 @@ export function normalizeTokenPrices(tokenPrices: IRawTokenPrices | undefined): return { default: normalized, longContext }; } - // No valid pricing data - return undefined; + // Legacy flat format (pre-2026-06-01): values are in nano-AIUs + if (tokenPrices.input_price === undefined || tokenPrices.output_price === undefined) { + return undefined; + } + return { + default: { + inputPrice: (tokenPrices.input_price / NANO_AIU_DIVISOR) * scale, + outputPrice: (tokenPrices.output_price / NANO_AIU_DIVISOR) * scale, + cachePrice: tokenPrices.cache_price !== undefined ? (tokenPrices.cache_price / NANO_AIU_DIVISOR) * scale : undefined, + cacheWritePrice: undefined, + }, + }; } diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts index ea6cf63308622..8d1efcc8df926 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -17,7 +17,7 @@ import { CustomDataPartMimeTypes } from '../../../platform/endpoint/common/endpo import { encodeStatefulMarker } from '../../../platform/endpoint/common/statefulMarkerContainer'; import { AutoChatEndpoint } from '../../../platform/endpoint/node/autoChatEndpoint'; import { IAutomodeService } from '../../../platform/endpoint/node/automodeService'; -import { CopilotChatEndpoint, CopilotUtilitySmallChatEndpoint } from '../../../platform/endpoint/node/copilotChatEndpoint'; +import { CopilotChatEndpoint } from '../../../platform/endpoint/node/copilotChatEndpoint'; import { IEnvService, isScenarioAutomation } from '../../../platform/env/common/envService'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { IOctoKitService } from '../../../platform/github/common/githubService'; @@ -32,6 +32,7 @@ import { ITelemetryService } from '../../../platform/telemetry/common/telemetry' import { isEncryptedThinkingDelta } from '../../../platform/thinking/common/thinking'; import { BaseTokensPerCompletion } from '../../../platform/tokenizer/node/tokenizer'; import { TelemetryCorrelationId } from '../../../util/common/telemetryCorrelationId'; +import { CancellationTokenSource } from '../../../util/vs/base/common/cancellation'; import { Emitter } from '../../../util/vs/base/common/event'; import { Disposable, MutableDisposable } from '../../../util/vs/base/common/lifecycle'; import { isBoolean, isDefined, isNumber, isString, isStringArray } from '../../../util/vs/base/common/types'; @@ -42,7 +43,7 @@ import { IExtensionContribution } from '../../common/contributions'; import { PromptRenderer } from '../../prompts/node/base/promptRenderer'; import { isImageDataPart } from '../common/languageModelChatMessageHelpers'; import { LanguageModelAccessPrompt } from './languageModelAccessPrompt'; -import { formatPricingLabel, formatTokenCount, getModelCapabilitiesDescription, buildReasoningEffortSchemaProperty } from '../common/languageModelAccess'; +import { formatPricingLabel, formatTokenCount, getAutoModelDescription, getAutoModelDiscountLabel, getModelCapabilitiesDescription, buildReasoningEffortSchemaProperty } from '../common/languageModelAccess'; /** * Markers in the autoModelHint experiment variable that indicate the auto model @@ -59,11 +60,12 @@ const experimentalAutoModelHintMarkers = ['minimax', 'mp3yn0h7', 'yaqq2gxh']; * model does not support configurable context sizes. * * Driven entirely by CAPI billing metadata: - * - When CAPI returns a `long_context` tier, offers `default.context_max` as - * the default option and `modelMaxPromptTokens` as an opt-in larger option. - * - When the long-context tier has higher prices, the larger option includes a - * cost indicator so the user knows they are opting into higher billing. - * - When there is no `long_context` tier, no selector is shown. + * - When CAPI returns a `long_context` tier with higher prices, offers + * `default.context_max` as the default and `modelMaxPromptTokens` as an + * opt-in larger option so the user knowingly opts into higher billing. + * - When there is no `long_context` tier, or prices match the default tier + * (i.e. `longContext` is absent on the pricing object), no selector is + * shown — the model silently uses the full context window. */ function getContextSizeOptions(endpoint: IChatEndpoint): { value: number; description: string; isDefault: boolean }[] | undefined { const pricing = endpoint.tokenPricing; @@ -84,13 +86,19 @@ function getContextSizeOptions(endpoint: IChatEndpoint): { value: number; descri const hasLongContextSurcharge = !!pricing.longContext; + // When both tiers cost the same, show only the full context window as a + // non-switchable indicator — the user always gets the larger window. + if (!hasLongContextSurcharge) { + return [ + { value: fullMax, description: vscode.l10n.t('Longer sessions'), isDefault: true }, + ]; + } + return [ { value: defaultMax, description: vscode.l10n.t('Default recommended context size'), isDefault: true }, { value: fullMax, - description: hasLongContextSurcharge - ? vscode.l10n.t('Longer sessions') - : vscode.l10n.t('Longer sessions without compaction'), + description: vscode.l10n.t('Longer sessions'), isDefault: false, }, ]; @@ -134,23 +142,6 @@ function buildConfigurationSchema(endpoint: IChatEndpoint): { configurationSchem const utilityAliasFamilies: readonly ChatEndpointFamily[] = ['copilot-utility-small', 'copilot-utility']; -/** - * Checks whether `endpoint` is the built-in Copilot endpoint for a utility alias. - */ -function isDefaultEndpointForUtilityFamily(family: ChatEndpointFamily, endpoint: IChatEndpoint): boolean { - if (!(endpoint instanceof CopilotChatEndpoint)) { - return false; - } - switch (family) { - case 'copilot-utility-small': - return endpoint.family === CopilotUtilitySmallChatEndpoint.capiFamily; - case 'copilot-utility': - return endpoint.isFallback; - default: - return false; - } -} - /** * Builds the {@link vscode.LanguageModelChatInformation} entry that publishes a * utility-family alias (e.g. `copilot-utility-small`) under the copilot vendor. @@ -222,6 +213,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib private _utilityAliasEndpoints: Map = new Map(); // Overrides resolved outside model-info publication, reused on the next alias publish. private readonly _resolvedUtilityEndpoints = new Map(); + private readonly _utilityOverridesRefresh = this._register(new MutableDisposable()); private _lmWrapper: CopilotLanguageModelWrapper; private _promptBaseCountCache: LanguageModelAccessPromptBaseCountCache; @@ -253,6 +245,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib } override dispose(): void { + this._utilityOverridesRefresh.value?.cancel(); super.dispose(); } @@ -276,7 +269,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib this._onDidChange.fire(); })); this._register(this._endpointProvider.onDidModelsRefresh(() => { - // Drop stale overrides; model publication uses defaults until refresh completes. + // Drop stale resolutions; aliases are re-published once the refresh re-resolves them. this._resolvedUtilityEndpoints.clear(); void this._refreshUtilityOverrides(); this._onDidChange.fire(); @@ -324,14 +317,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib if (endpoint.degradationReason) { modelTooltip = endpoint.degradationReason; } else if (endpoint instanceof AutoChatEndpoint) { - const baseAutoTooltip = vscode.l10n.t('Auto selects the best model based on your request complexity and model performance.'); - if (endpoint.discountRange.high === endpoint.discountRange.low && endpoint.discountRange.low !== 0) { - modelTooltip = `${baseAutoTooltip} ${vscode.l10n.t('Model use through Auto is billed at a {0}% discount.', endpoint.discountRange.low * 100)}`; - } else if (endpoint.discountRange.high !== endpoint.discountRange.low) { - modelTooltip = `${baseAutoTooltip} ${vscode.l10n.t('Model use through Auto is billed at a {0}% to {1}% discount.', endpoint.discountRange.low * 100, endpoint.discountRange.high * 100)}`; - } else { - modelTooltip = baseAutoTooltip; - } + modelTooltip = getAutoModelDescription(endpoint.discountRange); const isOrgManaged = !!this._authenticationService.copilotToken?.isManagedPlan; const autoModeHint = this._expService.getTreatmentVariable('copilotchat.autoModelHint'); const showExperimentalHint = !isOrgManaged && !!autoModeHint && experimentalAutoModelHintMarkers.some(marker => autoModeHint.includes(marker)); @@ -349,11 +335,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib let modelDetail: string | undefined; if (endpoint instanceof AutoChatEndpoint) { - if (endpoint.discountRange.high === endpoint.discountRange.low && endpoint.discountRange.low !== 0) { - modelDetail = `${endpoint.discountRange.low * 100}% discount`; - } else if (endpoint.discountRange.high !== endpoint.discountRange.low) { - modelDetail = `${endpoint.discountRange.low * 100}% to ${endpoint.discountRange.high * 100}% discount`; - } + modelDetail = getAutoModelDiscountLabel(endpoint.discountRange); } if (endpoint.customModel) { const customModel = endpoint.customModel; @@ -394,6 +376,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib [ApiChatLocation.Editor]: endpoint instanceof AutoChatEndpoint, // inline chat gets 'Auto' by default }, isUserSelectable: endpoint.showInModelPicker, + warningText: endpoint instanceof AutoChatEndpoint ? undefined : endpoint.warningText, capabilities: { imageInput: endpoint instanceof AutoChatEndpoint ? true : endpoint.supportsVision, toolCalling: endpoint.supportsToolCalls, @@ -407,14 +390,18 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib this._currentModels = models; this._chatEndpoints = chatEndpoints; - this._registerUtilityAliasModels(models, allEndpoints); + this._registerUtilityAliasModels(models); return models; } - /** Publishes utility aliases without waiting for override resolution. */ + /** + * Publishes utility aliases from the resolved-endpoint cache. The cache is + * populated asynchronously by {@link _refreshUtilityOverrides} (the single + * gate that decides, per the BYOK/override policy, whether a family resolves + * to a model at all), so a family with no cached endpoint publishes nothing. + */ private _registerUtilityAliasModels( models: vscode.LanguageModelChatInformation[], - allEndpoints: readonly IChatEndpoint[], ): void { this._utilityAliasEndpoints.clear(); const session = this._authenticationService.anyGitHubSession; @@ -422,15 +409,15 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib for (const family of utilityAliasFamilies) { const cached = this._resolvedUtilityEndpoints.get(family); - const endpoint = cached?.endpoint ?? allEndpoints.find(e => isDefaultEndpointForUtilityFamily(family, e)); - if (!endpoint) { + if (!cached) { continue; } + const endpoint = cached.endpoint; this._utilityAliasEndpoints.set(family, endpoint); try { // Copilot defaults clone an existing entry; synthesized override aliases need baseCount. - const aliasInfo = buildUtilityAliasModelInfo(family, endpoint, models, cached?.baseCount ?? 0, requiresAuthorization); + const aliasInfo = buildUtilityAliasModelInfo(family, endpoint, models, cached.baseCount, requiresAuthorization); this._logService.trace(`[LanguageModelAccess] Publishing alias '${family}' -> ${endpoint.model} (${aliasInfo.synthesized ? 'synthesized' : 'cloned'}, ${endpoint instanceof CopilotChatEndpoint ? 'copilot' : 'override'}).`); models.push(aliasInfo.info); } catch (err) { @@ -438,23 +425,44 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib } } - // Override resolution may hang, so keep it off the model-info request path. + // Resolution may hang (override lookups, base-count tokenization), so keep it off + // the model-info request path. Newly resolved endpoints are published on the next + // request once `_refreshUtilityOverrides` fires `_onDidChange`. void this._refreshUtilityOverrides().catch(err => { this._logService.warn(`[LanguageModelAccess] Failed to refresh utility overrides: ${err}`); }); } - /** Resolves configured utility model overrides for the next alias publish. */ + /** + * Resolves each utility family through {@link IEndpointProvider.getChatEndpoint}, + * which applies the BYOK/override policy (returning the configured override, the + * built-in Copilot model, or throwing when no model should be used). Successful + * resolutions are cached for {@link _registerUtilityAliasModels} to publish. + */ private async _refreshUtilityOverrides(): Promise { + this._utilityOverridesRefresh.value?.cancel(); + const cancellationSource = new CancellationTokenSource(); + this._utilityOverridesRefresh.value = cancellationSource; + const token = cancellationSource.token; let didChange = false; for (const family of utilityAliasFamilies) { let resolved: IChatEndpoint | undefined; try { resolved = await this._endpointProvider.getChatEndpoint(family); } catch (err) { - this._logService.warn(`[LanguageModelAccess] Failed to resolve utility alias '${family}' in background: ${err}`); + if (token.isCancellationRequested) { + return; + } + // Expected when the policy declines a family (e.g. BYOK main model with no + // configured utility model), so this is not necessarily a failure. The cache + // is cleared on policy changes via `onDidModelsRefresh`, so leaving any prior + // entry intact here only preserves a still-valid alias across transient errors. + this._logService.trace(`[LanguageModelAccess] No utility alias resolved for '${family}' in background: ${err}`); continue; } + if (token.isCancellationRequested) { + return; + } if (!resolved) { continue; } @@ -468,9 +476,15 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib try { baseCount = await this._promptBaseCountCache.getBaseCount(resolved); } catch (err) { + if (token.isCancellationRequested) { + return; + } this._logService.warn(`[LanguageModelAccess] Failed to compute baseCount for utility alias '${family}' -> ${resolved.model}; keeping previously-published alias. Error: ${err}`); continue; } + if (token.isCancellationRequested) { + return; + } this._resolvedUtilityEndpoints.set(family, { endpoint: resolved, baseCount }); didChange = true; } @@ -775,6 +789,15 @@ export class CopilotLanguageModelWrapper extends Disposable { const result = await wrappedRequest(); + if (result.type === ChatFetchResponseType.Length) { + // The model stopped generating because it hit the length/context-window limit + // (finish_reason "length"). The partial text has already been streamed to the + // consumer via the finished callback, so treat this as a successful (truncated) + // response instead of throwing "Response too long." and discarding the output. + this._logService.warn(`[LanguageModelAccess] Response from model '${_endpoint.model}' was truncated because it hit the length limit; returning the partial response.`); + return undefined; + } + if (result.type !== ChatFetchResponseType.Success) { if (result.type === ChatFetchResponseType.ExtensionBlocked) { if (extensionId) { diff --git a/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts b/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts index 0beeb61674998..35779997bb377 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts @@ -143,6 +143,41 @@ suite('CopilotLanguageModelWrapper', () => { assert.deepStrictEqual(decoded, expectedUsage); }); }); + + suite('length finish reason', () => { + let wrapper: CopilotLanguageModelWrapper; + let endpoint: IChatEndpoint; + let fetcher: MockChatMLFetcher; + setup(async () => { + createAccessor(); + fetcher = accessor.get(IChatMLFetcher) as MockChatMLFetcher; + endpoint = await accessor.get(IEndpointProvider).getChatEndpoint('copilot-utility'); + wrapper = instaService.createInstance(CopilotLanguageModelWrapper); + }); + + test('does not throw when the response is truncated due to length', async () => { + // A model (e.g. a custom/BYOK endpoint) can legitimately stop generating + // because it hit the context window ceiling, returning finish_reason "length" + // together with the partial text it already streamed. This must not surface + // as a hard "Response too long." failure that discards the generated text. + fetcher.setNextResponse({ + type: ChatFetchResponseType.Length, + reason: 'Response too long.', + requestId: 'test-request-id', + serverRequestId: 'test-server-request-id', + truncatedValue: 'partial answer' + }); + + await wrapper.provideLanguageModelResponse( + endpoint, + [vscode.LanguageModelChatMessage.User('hello')], + { requestInitiator: 'unknown', toolMode: vscode.LanguageModelChatToolMode.Auto }, + vscode.extensions.all[0].id, + { report: () => { } }, + CancellationToken.None + ); + }); + }); }); suite('LanguageModelAccess model info', () => { @@ -176,6 +211,7 @@ suite('LanguageModelAccess model info', () => { testingServiceCollection.define(IAutomodeService, { _serviceBrand: undefined, resolveAutoModeEndpoint: async () => endpoint, + consumeLastRoutingDecision: () => undefined, invalidateRouterCache: () => { }, } as unknown as IAutomodeService); testingServiceCollection.define(IEndpointProvider, { @@ -248,6 +284,89 @@ suite('LanguageModelAccess model info', () => { languageModelAccess.dispose(); } }); + + test('does not publish utility aliases when the provider declines to resolve them (BYOK)', async () => { + const testingServiceCollection = createExtensionTestingServices(); + testingServiceCollection.define(IEndpointProvider, { + _serviceBrand: undefined, + onDidModelsRefresh: Event.None, + getAllCompletionModels: async () => [], + getAllChatEndpoints: async () => [], + getChatEndpoint: async () => { throw new Error('No utility model is configured while the selected main model is BYOK.'); }, + getEmbeddingsEndpoint: async () => { throw new Error('Not implemented in test'); }, + } as unknown as IEndpointProvider); + const accessor = testingServiceCollection.createTestingAccessor(); + const languageModelAccess = accessor.get(IInstantiationService).createInstance(LanguageModelAccess); + const internals = languageModelAccess as unknown as { + _resolvedUtilityEndpoints: Map; + _promptBaseCountCache: { getBaseCount(endpoint: IChatEndpoint): Promise }; + _registerUtilityAliasModels(models: vscode.LanguageModelChatInformation[]): void; + _refreshUtilityOverrides(): Promise; + }; + internals._promptBaseCountCache = { getBaseCount: async () => 0 }; + + try { + // The provider gates every utility family, so nothing is resolved or cached. + await internals._refreshUtilityOverrides(); + assert.strictEqual(internals._resolvedUtilityEndpoints.size, 0); + + // With an empty cache, no aliases are published into the model list. + const models: vscode.LanguageModelChatInformation[] = []; + internals._registerUtilityAliasModels(models); + assert.deepStrictEqual(models.map(model => model.id), []); + } finally { + languageModelAccess.dispose(); + } + }); + + test('does not cache a utility endpoint from an obsolete refresh', async () => { + const endpoint = { + model: 'gpt-4o-mini', + modelProvider: 'copilot', + } as IChatEndpoint; + const baseCount = new DeferredPromise(); + const baseCountStarted = new DeferredPromise(); + let endpointRequestCount = 0; + const testingServiceCollection = createExtensionTestingServices(); + testingServiceCollection.define(IEndpointProvider, { + _serviceBrand: undefined, + onDidModelsRefresh: Event.None, + getAllCompletionModels: async () => [], + getAllChatEndpoints: async () => [], + getChatEndpoint: async () => { + if (endpointRequestCount++ === 0) { + return endpoint; + } + throw new Error('No utility model configured'); + }, + getEmbeddingsEndpoint: async () => { throw new Error('Not implemented in test'); }, + } as unknown as IEndpointProvider); + const accessor = testingServiceCollection.createTestingAccessor(); + const languageModelAccess = accessor.get(IInstantiationService).createInstance(LanguageModelAccess); + const internals = languageModelAccess as unknown as { + _resolvedUtilityEndpoints: Map; + _promptBaseCountCache: { getBaseCount(endpoint: IChatEndpoint): Promise }; + _refreshUtilityOverrides(): Promise; + }; + internals._promptBaseCountCache = { + getBaseCount: async () => { + baseCountStarted.complete(); + return baseCount.p; + } + }; + + try { + const obsoleteRefresh = internals._refreshUtilityOverrides(); + await baseCountStarted.p; + const currentRefresh = internals._refreshUtilityOverrides(); + baseCount.complete(0); + await Promise.all([obsoleteRefresh, currentRefresh]); + + assert.strictEqual(internals._resolvedUtilityEndpoints.size, 0); + } finally { + languageModelAccess.dispose(); + } + }); }); suite('buildUtilityAliasModelInfo', () => { @@ -445,6 +564,22 @@ suite('normalizeTokenPrices', () => { assert.ok(result.longContext, 'long-context tier should be included when cache_write_price differs'); assert.strictEqual(result.longContext?.cacheWritePrice, 3); }); + + test('converts legacy flat nano-AIU prices to credits per 1M tokens', () => { + // Shape returned by the cloud agents endpoint (/agents/swe/models) + const result = normalizeTokenPrices({ + batch_size: 1_000_000, + input_price: 500_000_000_000, + output_price: 2_500_000_000_000, + cache_price: 50_000_000_000, + }); + assert.ok(result); + assert.strictEqual(result.default.inputPrice, 500); + assert.strictEqual(result.default.outputPrice, 2500); + assert.strictEqual(result.default.cachePrice, 50); + assert.strictEqual(result.default.cacheWritePrice, undefined); + assert.strictEqual(result.longContext, undefined); + }); }); suite('formatPricingLabel', () => { @@ -464,4 +599,3 @@ suite('formatPricingLabel', () => { assert.strictEqual(formatPricingLabel(tier(3, 15)), 'In: 3 · Out: 15 AICs/1M tokens'); }); }); - diff --git a/extensions/copilot/src/extension/extension/vscode-node/services.ts b/extensions/copilot/src/extension/extension/vscode-node/services.ts index 6d304454c86d9..15ca799eec7c0 100644 --- a/extensions/copilot/src/extension/extension/vscode-node/services.ts +++ b/extensions/copilot/src/extension/extension/vscode-node/services.ts @@ -289,6 +289,7 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio // OTel service — resolve config from env + settings, create appropriate impl const otelSettings = workspace.getConfiguration('github.copilot.chat.otel'); + const policyValue = (key: string): T | undefined => (otelSettings.inspect(key) as { policyValue?: T } | undefined)?.policyValue; const otelConfig = resolveOTelConfig({ env: process.env, settingEnabled: otelSettings.get('enabled'), @@ -298,6 +299,19 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio settingMaxAttributeSizeChars: otelSettings.get('maxAttributeSizeChars'), settingOutfile: otelSettings.get('outfile') || undefined, settingDbSpanExporter: otelSettings.get('dbSpanExporter.enabled'), + settingProtocol: otelSettings.get('protocol') || undefined, + policyEnabled: policyValue('enabled'), + policyExporterType: policyValue<'otlp-grpc' | 'otlp-http' | 'console' | 'file'>('exporterType'), + policyOtlpEndpoint: policyValue('otlpEndpoint'), + policyCaptureContent: policyValue('captureContent'), + policyOutfile: policyValue('outfile'), + policyProtocol: policyValue('protocol'), + settingServiceName: otelSettings.get('serviceName') || undefined, + policyServiceName: policyValue('serviceName'), + settingResourceAttributes: otelSettings.get>('resourceAttributes'), + policyResourceAttributes: policyValue>('resourceAttributes'), + settingHeaders: otelSettings.get>('headers'), + policyHeaders: policyValue>('headers'), extensionVersion: extensionContext.extension.packageJSON.version ?? '0.0.0', sessionId: env.sessionId, }); diff --git a/extensions/copilot/src/extension/inlineEdits/node/continuousEnhancedTelemetrySender.ts b/extensions/copilot/src/extension/inlineEdits/node/continuousEnhancedTelemetrySender.ts index 9ee4f792802c3..8bdadf3647536 100644 --- a/extensions/copilot/src/extension/inlineEdits/node/continuousEnhancedTelemetrySender.ts +++ b/extensions/copilot/src/extension/inlineEdits/node/continuousEnhancedTelemetrySender.ts @@ -15,6 +15,48 @@ import { generateUuid } from '../../../util/vs/base/common/uuid'; import { DebugRecorder } from './debugRecorder'; import { NES_GH_TELEMETRY_EVENT_NAME } from './nextEditProviderTelemetry'; +/** + * The continuous-recording payload serialized into the `recording` telemetry + * property by {@link ContinuousEnhancedTelemetrySender._sendNow}. + * + * A continuous slice is a sliding window of recent edit activity; unlike a + * per-request ("alternative action") recording it has **no `requestTime`**. + */ +export interface IContinuousRecording { + /** + * The recorded edit timeline for the window, or `undefined` when the + * serialized entries exceed {@link + * ContinuousEnhancedTelemetrySender.MAX_ENTRIES_CHARS} — only `entriesSize` + * is shipped in that case. + */ + readonly entries: LogEntry[] | undefined; + + /** Size, in UTF-16 code units, of the serialized entries at capture time. */ + readonly entriesSize: number; + + /** + * Window start in epoch milliseconds (recorder clock). Adjacent slices can + * have gaps, so consecutive windows are not necessarily contiguous. + */ + readonly windowStart: number; + + /** Window end in epoch milliseconds (recorder clock). */ + readonly windowEnd: number; + + /** + * Identifies the sender instance. Stable per sender lifetime; a single + * extension session can span several ids (a new sender is created on + * copilot-token change, etc.). + */ + readonly sessionId: string; + + /** + * Slice index, monotonically increasing per `sessionId`. Not necessarily + * contiguous — empty slices are skipped at capture time. + */ + readonly sequenceNumber: number; +} + /** * Periodically sends an enhanced GH telemetry event with a fixed-length slice of recent workspace activity. * @@ -163,7 +205,7 @@ export class ContinuousEnhancedTelemetrySender extends Disposable { const entriesSize = entriesJson.length; const sequenceNumber = this._sequenceNumber++; - const recording = { + const recording: IContinuousRecording = { entries: entriesSize > ContinuousEnhancedTelemetrySender.MAX_ENTRIES_CHARS ? undefined : entries, entriesSize, windowStart, diff --git a/extensions/copilot/src/extension/inlineEdits/node/nextEditCache.ts b/extensions/copilot/src/extension/inlineEdits/node/nextEditCache.ts index 3659e0c210f89..bfc774395de64 100644 --- a/extensions/copilot/src/extension/inlineEdits/node/nextEditCache.ts +++ b/extensions/copilot/src/extension/inlineEdits/node/nextEditCache.ts @@ -33,10 +33,51 @@ export interface CachedEditOpts { * the cached entry is not served. */ cursorOffset?: number; + /** + * Zero-based index of the model-emitted patch this edit originated from + * (diff-patch response format). `undefined` for formats without an explicit + * patch structure. @see StreamedEdit.patchIndex + */ + patchIndex?: number; + /** + * Patch indices for the bundled `nextEdits`, aligned by position. Stored on the + * first cached entry so that edits served later via rebase (addressed by + * `rebasedEditIndex` into the bundle) can be attributed to the right model patch. + */ + patchIndices?: readonly (number | undefined)[]; + /** + * The document the cached edit actually applies to, when it differs from the + * document the entry is keyed under (cross-file NES). `undefined` means the edit + * targets the owning/key document (the common same-file case). + * + * Used to cache an `A -> suggestion-in-B` association: the entry is keyed and + * gated against the active document A (content + edit window), but the edit it + * carries lands in document B. + */ + targetDocId?: DocumentId; + /** + * The target document's content at the time the cross-file edit was produced + * (i.e. the content the {@link CachedEdit.edit} offsets index into). Only set + * together with {@link targetDocId}. The read path must serve the edit only when + * the target document's live content still equals this snapshot; otherwise the + * edit's offsets are stale and would resolve against the wrong content. + */ + targetDocumentBeforeEdit?: StringText; } export interface CachedEdit { docId: DocumentId; + /** + * The document the cached edit actually applies to, when it differs from {@link docId} + * (cross-file NES). `undefined` means the edit targets the owning/key document + * {@link docId} (the common same-file case). @see CachedEditOpts.targetDocId + */ + targetDocId?: DocumentId; + /** + * The target document's content at the time the cross-file edit was produced. + * Only set together with {@link targetDocId}. @see CachedEditOpts.targetDocumentBeforeEdit + */ + targetDocumentBeforeEdit?: StringText; documentBeforeEdit: StringText; editWindow?: OffsetRange; /** @@ -56,6 +97,17 @@ export interface CachedEdit { * When caching multiple edits, this is the order in which they were applied. */ subsequentN?: number; + /** + * Zero-based index of the model-emitted patch this edit originated from + * (diff-patch response format). @see StreamedEdit.patchIndex + */ + patchIndex?: number; + /** + * Patch indices for the bundled `edits`, aligned by position. Present on the + * first cached entry (the one carrying the `edits` bundle) so rebased subsequent + * edits, addressed by `rebasedEditIndex`, can be attributed to the right patch. + */ + patchIndices?: readonly (number | undefined)[]; source: NextEditFetchRequest; cacheTime: number; /** @@ -236,7 +288,7 @@ class DocumentEditCache { public setKthNextEdit(documentContents: StringText, editWindow: OffsetRange | undefined, nextEdit: StringReplacement, nextEdits: StringReplacement[] | undefined, userEditSince: StringEdit | undefined, subsequentN: number, source: NextEditFetchRequest, opts: CachedEditOpts): CachedEdit { const key = this._getKey(documentContents.value); - const cachedEdit: CachedEdit = { docId: this.docId, edit: nextEdit, edits: nextEdits, detailedEdits: [], userEditSince, subsequentN, source, documentBeforeEdit: documentContents, editWindow, originalEditWindow: opts.originalEditWindow, cacheTime: Date.now(), isFromCursorJump: opts.isFromCursorJump, cursorOffsetAtCacheTime: opts.cursorOffset }; + const cachedEdit: CachedEdit = { docId: this.docId, targetDocId: opts.targetDocId, targetDocumentBeforeEdit: opts.targetDocumentBeforeEdit, edit: nextEdit, edits: nextEdits, detailedEdits: [], userEditSince, subsequentN, patchIndex: opts.patchIndex, patchIndices: opts.patchIndices, source, documentBeforeEdit: documentContents, editWindow, originalEditWindow: opts.originalEditWindow, cacheTime: Date.now(), isFromCursorJump: opts.isFromCursorJump, cursorOffsetAtCacheTime: opts.cursorOffset }; if (userEditSince) { if (!checkEditConsistency(cachedEdit.documentBeforeEdit.value, userEditSince, this._doc.value.get().value, this._logger.createSubLogger('setKthNextEdit'))) { cachedEdit.userEditSince = undefined; @@ -286,7 +338,10 @@ class DocumentEditCache { // If the cursor moved farther from the edit's start line than it was at cache time, // reject the cached edit so the same suggestion is not shown again. // Only applies to non-rebased, non-subsequent edits. + // Skipped for cross-file entries: their `edit` is in the target document's + // coordinate space, so transforming it against this (active) document is meaningless. if (cacheCursorDistanceCheck + && !cachedEdit.targetDocId && cachedEdit.edit && (cachedEdit.subsequentN === undefined || cachedEdit.subsequentN === 0) && cachedEdit.cursorOffsetAtCacheTime !== undefined diff --git a/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts b/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts index e13a6facef618..bdcc6685e8b5f 100644 --- a/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts @@ -97,6 +97,7 @@ function createDocStateLookupMap(projectedDocuments: readonly ProcessedDoc[], xt docContents: StringText; editsSoFar: StringEdit; nextEdits: StringReplacement[]; + patchIndices: (number | undefined)[]; docId: DocumentId; }> { const statePerDoc = new CachedFunction((id: DocumentId) => { @@ -111,6 +112,7 @@ function createDocStateLookupMap(projectedDocuments: readonly ProcessedDoc[], xt docContents: baseDocState, editsSoFar: StringEdit.empty, nextEdits: [] as StringReplacement[], + patchIndices: [] as (number | undefined)[], docId: id, }; } @@ -122,6 +124,7 @@ function createDocStateLookupMap(projectedDocuments: readonly ProcessedDoc[], xt docContents: doc.documentAfterEdits, editsSoFar: StringEdit.empty, nextEdits: [] as StringReplacement[], + patchIndices: [] as (number | undefined)[], docId: id, }; }); @@ -130,6 +133,23 @@ function createDocStateLookupMap(projectedDocuments: readonly ProcessedDoc[], xt return statePerDoc; } +/** + * Computes the originating model-patch index for a served edit. In the rebase path + * the served edit is addressed by `rebasedEditIndex` into the entry's bundled + * `patchIndices`; otherwise the entry carries its own `patchIndex`. + * + * Invariant: `rebasedEditIndex` is only ever set for entry 0 (the sole entry given a + * `patchIndices` bundle), so whenever it is defined `patchIndices` is defined too. + * We therefore deliberately do NOT fall back to `patchIndex` in the rebase branch: a + * served bundle slot of `undefined` is a genuine "no originating patch" attribution + * and must not be masked by entry 0's own patch index. + */ +function getSourcePatchIndex(cachedEdit: CachedOrRebasedEdit): number | undefined { + return cachedEdit.rebasedEditIndex !== undefined + ? cachedEdit.patchIndices?.[cachedEdit.rebasedEditIndex] + : cachedEdit.patchIndex; +} + export interface NESInlineCompletionContext extends vscode.InlineCompletionContext { enforceCacheDelay: boolean; changeHint?: NesChangeHint; @@ -321,7 +341,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider suggestion-in-targetDoc` + * association. This lets the cross-file suggestion be re-served from cache while the cursor + * is still in the active document (the regular path only caches it under the target + * document, which is reachable from cache only after the user jumps there). + * + * The entry is stored without `userEditSince`, so it is never tracked/rebased: its edit is + * in the target document's coordinate space and is served by exact content match only. + */ + private _maybeCacheCrossFileEditUnderActiveDoc( + ithEdit: number, + activeDocId: DocumentId, + activeDocContents: StringText, + activeDocEditWindow: OffsetRange | undefined, + targetDocId: DocumentId, + targetDocContents: StringText, + nextEdit: StringReplacement, + streamedEdit: { readonly isFromCursorJump: boolean; readonly originalWindow?: OffsetRange; readonly patchIndex?: number }, + source: NextEditFetchRequest, + ): boolean { + if (ithEdit !== 0 || targetDocId === activeDocId) { + return false; // only the first streamed edit, and only when it targets a different document + } + this._nextEditCache.setKthNextEdit( + activeDocId, + activeDocContents, + activeDocEditWindow, + nextEdit, + 0, + undefined, // no bundled edits: served by exact content match only + undefined, // no userEditSince: never tracked/rebased (edit is in target-doc coords) + source, + { isFromCursorJump: streamedEdit.isFromCursorJump, originalEditWindow: streamedEdit.originalWindow, patchIndex: streamedEdit.patchIndex, targetDocId, targetDocumentBeforeEdit: targetDocContents } + ); + return true; + } + private determineNesConfigs(telemetryBuilder: LlmNESTelemetryBuilder, logContext: InlineEditRequestLogContext): INesConfigs { const nesConfigs: INesConfigs = { isAsyncCompletions: this._configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsAsyncCompletions, this._expService), @@ -749,7 +838,12 @@ export class NextEditProvider extends Disposable implements INextEditProvider { + // Tracks whether this stream stored a cross-file suggestion under the active document. + // When it did, the active document must NOT be cached as "no edit" at stream end — + // that would clobber the cross-file entry stored under the same key. + let didCacheCrossFileActiveDocEntry = false; + + const processEdit = (streamedEdit: { readonly edit: LineReplacement; readonly isFromCursorJump: boolean; readonly window?: OffsetRange; readonly originalWindow?: OffsetRange; readonly targetDocument?: DocumentId; readonly patchIndex?: number }, telemetry: IStatelessNextEditTelemetry): CachedOrRebasedEdit | undefined => { ++ithEdit; const myLogger = logger.createSubLogger('processEdit'); myLogger.trace(`processing edit #${ithEdit} (starts at 0)`); @@ -784,6 +878,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider { readonly isFromCache: boolean; readonly reusedRequest: ReusedRequestKind | undefined; readonly subsequentEditOrder: number | undefined; + readonly sourcePatchIndex: number | undefined; readonly activeDocumentOriginalLineCount: number | undefined; readonly activeDocumentEditsCount: number | undefined; readonly activeDocumentLanguageId: string | undefined; @@ -244,6 +245,7 @@ export class LlmNESTelemetryBuilder extends Disposable { isFromCache: this._isFromCache, reusedRequest: this._reusedRequest, subsequentEditOrder: this._subsequentEditOrder, + sourcePatchIndex: this._sourcePatchIndex, documentsCount, editsCount, activeDocumentEditsCount, @@ -354,6 +356,12 @@ export class LlmNESTelemetryBuilder extends Disposable { return this; } + private _sourcePatchIndex: number | undefined; + public setSourcePatchIndex(sourcePatchIndex: number | undefined): this { + this._sourcePatchIndex = sourcePatchIndex; + return this; + } + private _request: StatelessNextEditRequest | undefined; public setRequest(request: StatelessNextEditRequest): this { this._request = request; @@ -1006,6 +1014,7 @@ export class TelemetrySender implements IDisposable { isFromCache, reusedRequest, subsequentEditOrder, + sourcePatchIndex, activeDocumentLanguageId, activeDocumentOriginalLineCount, nLinesOfCurrentFileInPrompt, @@ -1107,6 +1116,7 @@ export class TelemetrySender implements IDisposable { "isFromCache": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit was provided from cache", "isMeasurement": true }, "reusedRequest": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the result was obtained by joining a pending request ('speculative' or 'async'), undefined for fresh requests and cache hits" }, "subsequentEditOrder": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Order of the subsequent edit", "isMeasurement": true }, + "sourcePatchIndex": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Zero-based index of the model-emitted patch this served edit originated from (diff-patch format). A single model patch can expand into multiple edits that share this index; undefined for formats without explicit patches.", "isMeasurement": true }, "activeDocumentOriginalLineCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of lines in the active document before shortening", "isMeasurement": true }, "activeDocumentNLinesInPrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of lines in the active document included in prompt", "isMeasurement": true }, "wasPreviouslyRejected": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the edit was previously rejected", "isMeasurement": true }, @@ -1206,6 +1216,7 @@ export class TelemetrySender implements IDisposable { nextEditProviderDuration, isFromCache: this._boolToNum(isFromCache), subsequentEditOrder, + sourcePatchIndex, activeDocumentOriginalLineCount, activeDocumentNLinesInPrompt: nLinesOfCurrentFileInPrompt, wasPreviouslyRejected: this._boolToNum(wasPreviouslyRejected), diff --git a/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderCaching.spec.ts b/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderCaching.spec.ts index 5881855dde1ba..9f0a7d82c2d31 100644 --- a/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderCaching.spec.ts +++ b/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderCaching.spec.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { outdent } from 'outdent'; import { afterAll, assert, beforeAll, describe, expect, it } from 'vitest'; -import { IConfigurationService } from '../../../../platform/configuration/common/configurationService'; +import { ConfigKey, ExperimentBasedConfig, ExperimentBasedConfigType, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; import { DefaultsOnlyConfigurationService } from '../../../../platform/configuration/common/defaultsOnlyConfigurationService'; +import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService'; import { IGitExtensionService } from '../../../../platform/git/common/gitExtensionService'; import { NullGitExtensionService } from '../../../../platform/git/common/nullGitExtensionService'; import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId'; @@ -24,6 +25,7 @@ import { mockNotebookService } from '../../../../platform/test/common/testNotebo import { TestWorkspaceService } from '../../../../platform/test/node/testWorkspaceService'; import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService'; import { Result } from '../../../../util/common/result'; +import { DeferredPromise, timeout } from '../../../../util/vs/base/common/async'; import { CancellationToken } from '../../../../util/vs/base/common/cancellation'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { URI } from '../../../../util/vs/base/common/uri'; @@ -58,7 +60,7 @@ describe('NextEditProvider Caching', () => { afterAll(() => { disposableStore.dispose(); }); - function createStatelessNextEditProvider(): IStatelessNextEditProvider { + function createStatelessNextEditProvider(patchIndices?: readonly (number | undefined)[]): IStatelessNextEditProvider { return { ID: 'TestNextEditProvider', provideNextEdit: async function*(request: StatelessNextEditRequest, logger: ILogger, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken) { @@ -83,8 +85,11 @@ describe('NextEditProvider Caching', () => { ) ] ); + let editIndex = 0; for (const edit of lineEdit.replacements) { - yield new WithStatelessProviderTelemetry({ targetDocument: request.getActiveDocument().id, edit, isFromCursorJump: false }, telemetryBuilder.build(Result.ok(undefined))); + const patchIndex = patchIndices ? patchIndices[editIndex] : undefined; + editIndex++; + yield new WithStatelessProviderTelemetry({ targetDocument: request.getActiveDocument().id, edit, isFromCursorJump: false, patchIndex }, telemetryBuilder.build(Result.ok(undefined))); } const noSuggestions = new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, undefined); return new WithStatelessProviderTelemetry(noSuggestions, telemetryBuilder.build(Result.error(noSuggestions))); @@ -331,4 +336,287 @@ describe('NextEditProvider Caching', () => { expect(secondCacheEntry).toBe(firstCacheEntry); expect(secondCacheEntry.wasRenderedAsInlineSuggestion).toBe(true); }); + + it('attributes each served edit to its originating model patch via sourcePatchIndex', async () => { + const obsWorkspace = new MutableObservableWorkspace(); + const obsGit = new ObservableGit(gitExtensionService); + // Edits are served in line order: the z parameter (patch 0), the getDistance + // body (also patch 0, i.e. a split of the same model patch per PR #322438), + // then the variable declaration (patch 1). + const statelessNextEditProvider = createStatelessNextEditProvider([0, 0, 1]); + + const nextEditProvider: NextEditProvider = new NextEditProvider(obsWorkspace, statelessNextEditProvider, new NesHistoryContextProvider(obsWorkspace, obsGit), new NesXtabHistoryTracker(obsWorkspace, undefined, configService, expService), undefined, configService, snippyService, logService, expService, requestLogger); + + const doc = obsWorkspace.addDocument({ + id: DocumentId.create(URI.file('/test/test.ts').toString()), + initialValue: outdent` + class Point { + constructor( + private readonly x: number, + private readonly y: number, + ) { } + getDistance() { + return Math.sqrt(this.x ** 2 + this.y ** 2); + } + } + + const myPoint = new Point(0, 1);`.trimStart() + }); + doc.setSelection([new OffsetRange(1, 1)], undefined); + doc.applyEdit(StringEdit.insert(11, '3D')); + + const context: NESInlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200, enforceCacheDelay: false }; + const logContext = new InlineEditRequestLogContext(doc.id.toString(), 1, context); + const cancellationToken = CancellationToken.None; + + const servedPatchIndices: (number | undefined)[] = []; + for (let i = 0; i < 3; i++) { + const tb = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc); + const result = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb.nesBuilder); + assert(result.result?.edit, `expected an edit on call ${i + 1}`); + servedPatchIndices.push(tb.nesBuilder.build(false).sourcePatchIndex); + tb.dispose(); + doc.applyEdit(result.result.edit.toEdit()); + } + + // The first (fresh) edit must be attributed too, not just the cached ones; + // the split pair shares patch 0 while the final edit comes from patch 1. + expect(servedPatchIndices).toEqual([0, 0, 1]); + }); + + /** + * Configuration that disables the cross-document cache purge (which otherwise deletes + * cross-file cache entries whenever *any* document is edited, by setting + * `InlineEditsTriggerOnEditorChangeAfterSeconds` to `undefined`). With the purge off, a + * cross-file entry survives an edit to its target document, so the read-path staleness guard + * — rather than the purge — becomes responsible for not serving the now-stale suggestion. + */ + class PurgeDisabledConfigurationService extends InMemoryConfigurationService { + override getExperimentBasedConfig(key: ExperimentBasedConfig, experimentationService: IExperimentationService): T { + if (key === ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds) { + return undefined as T; + } + return super.getExperimentBasedConfig(key, experimentationService); + } + } + + /** + * Stateless provider that yields a single edit targeting a *different* document than the + * active one, and only on its first invocation. Later invocations yield nothing, so any + * subsequently served suggestion can only originate from the cache. + * + * Exposes the number of times it was invoked (to distinguish a cache hit from a fresh + * fetch) and a promise that resolves once the first request's stream has fully ended (so + * tests can re-request only after the no-suggestions stream-end handling has run). + */ + function createCrossFileStatelessProvider(targetDocId: DocumentId, targetEdit: LineReplacement, activeDocWindow?: OffsetRange): { provider: IStatelessNextEditProvider; getCallCount: () => number; whenFirstStreamEnded: Promise } { + let callCount = 0; + const firstStreamEnded = new DeferredPromise(); + const provider: IStatelessNextEditProvider = { + ID: 'TestCrossFileNextEditProvider', + provideNextEdit: async function*(request: StatelessNextEditRequest, logger: ILogger, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken) { + const telemetryBuilder = new StatelessNextEditTelemetryBuilder(request.headerRequestId); + callCount++; + const isFirstCall = callCount === 1; + try { + if (isFirstCall) { + yield new WithStatelessProviderTelemetry({ targetDocument: targetDocId, edit: targetEdit, isFromCursorJump: false, window: activeDocWindow, patchIndex: undefined }, telemetryBuilder.build(Result.ok(undefined))); + } + const noSuggestions = new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, undefined); + return new WithStatelessProviderTelemetry(noSuggestions, telemetryBuilder.build(Result.error(noSuggestions))); + } finally { + if (isFirstCall) { + firstStreamEnded.complete(); + } + } + } + }; + return { provider, getCallCount: () => callCount, whenFirstStreamEnded: firstStreamEnded.p }; + } + + async function runCrossFileScenario(options?: { activeDocWindow?: OffsetRange; disposeTargetBeforeSecondRequest?: boolean; mutateTargetBeforeSecondRequest?: boolean; disableEditorChangeTrigger?: boolean }) { + const obsWorkspace = new MutableObservableWorkspace(); + const obsGit = new ObservableGit(gitExtensionService); + + // DocumentId is interned, so we can compute the ids before adding the documents. + const docAId = DocumentId.create(URI.file('/test/a.ts').toString()); + const docBId = DocumentId.create(URI.file('/test/b.ts').toString()); + + // By default the shared (defaults-only) config keeps the cross-document cache purge on. + // Opt into the purge-disabled config to isolate the read-path staleness guard. + const scenarioConfigService = options?.disableEditorChangeTrigger ? new PurgeDisabledConfigurationService(new DefaultsOnlyConfigurationService()) : configService; + + // Suggestion (for the non-active document B) replacing its `return 1;` line. + const targetEdit = new LineReplacement(new LineRange(2, 3), ['\treturn 42;']); + const { provider: statelessNextEditProvider, getCallCount, whenFirstStreamEnded } = createCrossFileStatelessProvider(docBId, targetEdit, options?.activeDocWindow); + + const nextEditProvider: NextEditProvider = new NextEditProvider(obsWorkspace, statelessNextEditProvider, new NesHistoryContextProvider(obsWorkspace, obsGit), new NesXtabHistoryTracker(obsWorkspace, undefined, scenarioConfigService, expService), undefined, scenarioConfigService, snippyService, logService, expService, requestLogger); + + const docB = obsWorkspace.addDocument({ id: docBId, initialValue: ['export function helper() {', '\treturn 1;', '}'].join('\n') }); + const docA = obsWorkspace.addDocument({ id: docAId, initialValue: ['class Point {', '\tconstructor(', '\t\tprivate readonly x: number,', '\t) { }', '}'].join('\n') }); + + docB.setSelection([new OffsetRange(0, 0)], undefined); + // The active document's cursor sits at offset 1 — relevant to the edit-window gating tests. + docA.setSelection([new OffsetRange(1, 1)], undefined); + + // Give document B a recent edit so it is part of the active document's history + // context (required for the cross-file edit to be processed against it). Append at + // the end so B's `return 1;` line stays put. + docB.applyEdit(StringEdit.insert(docB.value.get().value.length, '\n// touched')); + // Edit document A so it is the active document and has history ("Point" -> "Point3D"). + docA.applyEdit(StringEdit.insert(11, '3D')); + + const context: NESInlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200, enforceCacheDelay: false }; + const logContext = new InlineEditRequestLogContext(docA.id.toString(), 1, context); + const cancellationToken = CancellationToken.None; + + const tb1 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, docA); + const first = await nextEditProvider.getNextEdit(docA.id, context, logContext, cancellationToken, tb1.nesBuilder); + tb1.dispose(); + + // Wait until the first request's stream has fully ended before re-requesting. The + // cross-file edit is processed synchronously (so `first` already has it), but the + // terminal no-suggestions handling — which clears the pending request and would clobber + // the active-doc cache entry via `setNoNextEdit` if not guarded — runs afterwards in the + // background. Draining it here makes the second request deterministically observe the + // post-stream-end cache state (cache hit vs. fresh fetch), so these tests are not vacuous. + await whenFirstStreamEnded; + await timeout(0); + + // Optionally close the target document B before re-requesting, so the cached cross-file + // entry can no longer be resolved against live content. + if (options?.disposeTargetBeforeSecondRequest) { + docB.dispose(); + } + // Optionally mutate the target document B so its live content no longer matches the + // snapshot the cross-file edit was produced against, making the cached edit stale. + if (options?.mutateTargetBeforeSecondRequest) { + docB.applyEdit(StringEdit.insert(0, '// header\n')); + } + + // Second request with no change to the active document A: the provider is now silent, + // so any served suggestion must come from the cache. + const tb2 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, docA); + const second = await nextEditProvider.getNextEdit(docA.id, context, logContext, cancellationToken, tb2.nesBuilder); + tb2.dispose(); + + // Tear down the provider (which registers autoruns/watchers on `openDocuments` in its + // constructor) and the documents so each scenario run is self-contained and does not + // accumulate observers across the cross-file tests. Dispose the provider first so its + // autoruns no longer react to the documents being removed; document disposal is + // idempotent, so re-disposing `docB` after `disposeTargetBeforeSecondRequest` is safe. + nextEditProvider.dispose(); + docA.dispose(); + docB.dispose(); + + return { first, second, docBId, getCallCount }; + } + + it('re-serves a cross-file suggestion from cache while the cursor stays in the active document (surviving the no-suggestions stream end)', async () => { + const { first, second, docBId, getCallCount } = await runCrossFileScenario(); + + // Fresh: the provider produced a suggestion that targets the other document. + assert(first.result?.edit, 'expected a cross-file edit on the first request'); + expect(first.result.targetDocumentId).toBe(docBId); + + // Cached: re-served from the entry keyed under the active document, even though the + // first request's stream ended with no suggestions *for the active document* (which must + // not clobber the cross-file entry) and the provider no longer yields anything. It is the + // very same suggestion (and target), served from cache without re-invoking the provider. + assert(second.result?.edit, 'expected the cross-file edit to be re-served from cache'); + expect(second.result.targetDocumentId).toBe(docBId); + expect(second.result.edit).toBe(first.result.edit); + expect(getCallCount()).toBe(1); + }); + + it('re-serves a cross-file suggestion from cache when the cursor is within the cached edit window', async () => { + // The active document's cursor is at offset 1, which falls inside [0, 14). + const { first, second, docBId, getCallCount } = await runCrossFileScenario({ activeDocWindow: new OffsetRange(0, 14) }); + + assert(first.result?.edit, 'expected a cross-file edit on the first request'); + expect(first.result.targetDocumentId).toBe(docBId); + + assert(second.result?.edit, 'expected the cross-file edit to be re-served from cache'); + expect(second.result.targetDocumentId).toBe(docBId); + expect(second.result.edit).toBe(first.result.edit); + expect(getCallCount()).toBe(1); + }); + + it('does not re-serve a cross-file suggestion from cache when the cursor is outside the cached edit window', async () => { + // The active document's cursor is at offset 1, which is outside [20, 30). Window gating + // only applies to cache hits, so the fresh suggestion is unaffected. + const { first, second, docBId, getCallCount } = await runCrossFileScenario({ activeDocWindow: new OffsetRange(20, 30) }); + + assert(first.result?.edit, 'expected a cross-file edit on the first request'); + expect(first.result.targetDocumentId).toBe(docBId); + + // The cached entry is gated out by the edit window, so the second request misses the + // cache and falls through to a fresh fetch (a second provider call), which is silent. + expect(second.result?.edit).toBeUndefined(); + expect(getCallCount()).toBe(2); + }); + + it('refetches instead of serving a stale cross-file suggestion when the target document is no longer open', async () => { + // Same setup as the happy path, but the target document B is closed before the second + // request. The only difference from the happy path (which serves from cache with a single + // provider call) is the closed target, so a second provider call proves the closed-target + // validity check turned the cache hit into a miss rather than serving an unplaceable edit. + const { first, second, docBId, getCallCount } = await runCrossFileScenario({ disposeTargetBeforeSecondRequest: true }); + + assert(first.result?.edit, 'expected a cross-file edit on the first request'); + expect(first.result.targetDocumentId).toBe(docBId); + + // The cross-file entry is found but its target document is closed, so it cannot be + // resolved against live content; the read path treats it as a cache miss and refetches + // (a second, silent provider call) instead of getting stuck re-serving the dead entry. + expect(second.result?.edit).toBeUndefined(); + expect(getCallCount()).toBe(2); + }); + + it('re-serves a cross-file suggestion (with the purge disabled) while the target document is unchanged', async () => { + // Control for the staleness test below: with the cross-document purge disabled, the + // cross-file entry survives into the second request, and while the target document is + // unchanged it is served straight from cache (a single provider call). This isolates the + // staleness guard — any refetch in the sibling test must come from the content change, + // not from the entry being absent. + const { first, second, docBId, getCallCount } = await runCrossFileScenario({ disableEditorChangeTrigger: true }); + + assert(first.result?.edit, 'expected a cross-file edit on the first request'); + expect(first.result.targetDocumentId).toBe(docBId); + + assert(second.result?.edit, 'expected the cross-file edit to be re-served from cache'); + expect(second.result.targetDocumentId).toBe(docBId); + expect(second.result.edit).toBe(first.result.edit); + expect(getCallCount()).toBe(1); + }); + + it('refetches instead of serving a stale cross-file suggestion when the target document changed since it was produced', async () => { + // Disable the cross-document purge so the cross-file entry survives the edit to B; the + // read-path staleness guard is then the only thing standing between a changed target and a + // misplaced suggestion. Paired with the unchanged-target control above, the extra provider + // call here is attributable to the content change rather than to a missing entry. + const { first, second, docBId, getCallCount } = await runCrossFileScenario({ mutateTargetBeforeSecondRequest: true, disableEditorChangeTrigger: true }); + + assert(first.result?.edit, 'expected a cross-file edit on the first request'); + expect(first.result.targetDocumentId).toBe(docBId); + + // The target document changed after the suggestion was produced, so the cached edit's + // offsets are stale and would land at the wrong location. The read path treats the hit as + // a cache miss and refetches (a second provider call) rather than serving a misplaced edit. + expect(second.result?.edit).toBeUndefined(); + expect(getCallCount()).toBe(2); + }); + + it('does not serve a stale cross-file suggestion when the target document changed under the default (purge-enabled) configuration', async () => { + // In the default configuration, editing the target document B purges every cross-document + // cache entry (including the active-document-keyed cross-file entry) before the next + // request, so the stale suggestion is never re-served and the provider is consulted again. + const { first, second, docBId, getCallCount } = await runCrossFileScenario({ mutateTargetBeforeSecondRequest: true }); + + assert(first.result?.edit, 'expected a cross-file edit on the first request'); + expect(first.result.targetDocumentId).toBe(docBId); + + expect(second.result?.edit).toBeUndefined(); + expect(getCallCount()).toBe(2); + }); }); diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 3d1311851f272..b4446bb114a8e 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -41,7 +41,7 @@ import { Iterable } from '../../../util/vs/base/common/iterator'; import { DisposableMap, DisposableStore } from '../../../util/vs/base/common/lifecycle'; import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation'; -import { ChatResponseProgressPart2 } from '../../../vscodeTypes'; +import { ChatResponseAutoModeResolutionPart, ChatResponseProgressPart2 } from '../../../vscodeTypes'; import { ICommandService } from '../../commands/node/commandService'; import { Intent } from '../../common/constants'; import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection'; @@ -96,16 +96,30 @@ function isResponsesCompactionContextManagementEnabled(endpoint: IChatEndpoint, * applied on the `vscode.lm` path in `languageModelAccess.ts`. * * Only clamps when the selection is strictly smaller than the model window so - * the full tier ("Longer sessions without compaction") stays uncompacted. + * the full tier ("Longer sessions") stays uncompacted. + * + * When no explicit selection is present and the model has a long-context + * surcharge, falls back to the model's default context-max tier + * (`tokenPricing.default.contextMax`). When both tiers cost the same (no + * `longContext` pricing tier), skips the fallback and uses the full native + * window — users get long context for free. * * @internal - exported for testing */ export function applyContextSizeOverride(endpoint: IChatEndpoint, request: vscode.ChatRequest): IChatEndpoint { const contextSize = request.modelConfiguration?.contextSize; - // Guard against non-positive / non-finite selections (e.g. 0, -1, NaN, Infinity): - // a non-positive token budget would produce an invalid endpoint configuration. - if (typeof contextSize === 'number' && Number.isFinite(contextSize) && contextSize > 0 && contextSize < endpoint.modelMaxPromptTokens) { - return endpoint.cloneWithTokenOverride(contextSize); + // Use the explicit selection when valid, otherwise fall back to the default + // context-max tier. Guard against non-positive / non-finite selections + // (e.g. 0, -1, NaN, Infinity): a non-positive token budget would produce an + // invalid endpoint configuration. + // When both tiers cost the same (no longContext pricing tier), skip the + // fallback and use the full model window — users get long context for free. + const hasLongContextSurcharge = !!endpoint.tokenPricing?.longContext; + const effectiveSize = (typeof contextSize === 'number' && Number.isFinite(contextSize) && contextSize > 0) + ? contextSize + : hasLongContextSurcharge ? endpoint.tokenPricing?.default.contextMax : undefined; + if (typeof effectiveSize === 'number' && effectiveSize > 0 && effectiveSize < endpoint.modelMaxPromptTokens) { + return endpoint.cloneWithTokenOverride(effectiveSize); } return endpoint; } @@ -231,12 +245,16 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. allowTools[ToolName.CoreRunTest] = await testService.hasAnyTests(); allowTools[ToolName.CoreRunTask] = tasksService.getTasks().length > 0; - // The specialized subagents must only run when - // the main agent is on CAPI. + // The specialized subagents and semantic search only work when the main + // agent is on CAPI. semantic_search relies on embeddings that require a + // Copilot token source, so on BYOK / custom endpoints it can abort the chat + // turn (e.g. when the GitHub auth provider is unavailable). Keep it off + // there. See https://github.com/microsoft/vscode/issues/322525. if (!isCAPIEndpoint(model)) { allowTools[ToolName.SearchSubagent] = false; allowTools[ToolName.ExploreSubagent] = false; allowTools[ToolName.ExecutionSubagent] = false; + allowTools[ToolName.Codebase] = false; } else { const searchSubagentEnabled = configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SearchSubagentToolEnabled, experimentationService); const exploreAgentEnabled = configurationService.getExperimentBasedConfig(ConfigKey.ExploreAgentEnabled, experimentationService); @@ -446,6 +464,12 @@ export class AgentIntent extends EditCodeIntent { return this.handleSummarizeCommand(conversation, request, stream, token); } + // Report auto-mode routing decision if one was made during endpoint resolution + const routingDecision = this._automodeService.consumeLastRoutingDecision(); + if (routingDecision) { + stream.push(new ChatResponseAutoModeResolutionPart(routingDecision.resolvedModel, routingDecision.resolvedModelName, routingDecision.predictedLabel, routingDecision.confidence)); + } + try { return await super.handleRequest(conversation, request, stream, token, documentContext, agentName, location, chatTelemetry, yieldRequested); } finally { diff --git a/extensions/copilot/src/extension/intents/node/test/contextSizeOverride.spec.ts b/extensions/copilot/src/extension/intents/node/test/contextSizeOverride.spec.ts index 0bdcf1ff53f1f..364a9ee17478d 100644 --- a/extensions/copilot/src/extension/intents/node/test/contextSizeOverride.spec.ts +++ b/extensions/copilot/src/extension/intents/node/test/contextSizeOverride.spec.ts @@ -9,10 +9,11 @@ import { IChatEndpoint } from '../../../../platform/networking/common/networking import { applyContextSizeOverride } from '../agentIntent'; describe('applyContextSizeOverride', () => { - function createEndpoint(modelMaxPromptTokens: number): { endpoint: IChatEndpoint; clonedWith: number[] } { + function createEndpoint(modelMaxPromptTokens: number, defaultContextMax?: number, longContext?: { inputPrice: number }): { endpoint: IChatEndpoint; clonedWith: number[] } { const clonedWith: number[] = []; const endpoint = { modelMaxPromptTokens, + tokenPricing: defaultContextMax === undefined ? undefined : { default: { contextMax: defaultContextMax }, longContext }, cloneWithTokenOverride(tokens: number): IChatEndpoint { clonedWith.push(tokens); return createEndpoint(tokens).endpoint; @@ -39,13 +40,39 @@ describe('applyContextSizeOverride', () => { expect(clonedWith).toEqual([]); }); - test('does not clamp when context size is unset or non-numeric', () => { + test('does not clamp when context size is unset or non-numeric and the model has no default tier', () => { const { endpoint, clonedWith } = createEndpoint(400_000); expect(applyContextSizeOverride(endpoint, createRequest(undefined))).toBe(endpoint); expect(applyContextSizeOverride(endpoint, createRequest('big'))).toBe(endpoint); expect(clonedWith).toEqual([]); }); + test('falls back to the default context-max tier when no selection is present and there is a surcharge', () => { + const { endpoint, clonedWith } = createEndpoint(1_000_000, 200_000, { inputPrice: 6 }); + expect(applyContextSizeOverride(endpoint, createRequest(undefined)).modelMaxPromptTokens).toBe(200_000); + expect(applyContextSizeOverride(endpoint, createRequest('big')).modelMaxPromptTokens).toBe(200_000); + expect(clonedWith).toEqual([200_000, 200_000]); + }); + + test('an explicit selection wins over the default tier fallback', () => { + const { endpoint, clonedWith } = createEndpoint(1_000_000, 200_000, { inputPrice: 6 }); + expect(applyContextSizeOverride(endpoint, createRequest(500_000)).modelMaxPromptTokens).toBe(500_000); + expect(clonedWith).toEqual([500_000]); + }); + + test('does not clamp when long context has no surcharge (same price)', () => { + const { endpoint, clonedWith } = createEndpoint(1_000_000, 200_000); + expect(applyContextSizeOverride(endpoint, createRequest(undefined))).toBe(endpoint); + expect(applyContextSizeOverride(endpoint, createRequest('big'))).toBe(endpoint); + expect(clonedWith).toEqual([]); + }); + + test('does not clamp when the default tier equals or exceeds the model window', () => { + const { endpoint, clonedWith } = createEndpoint(200_000, 200_000); + expect(applyContextSizeOverride(endpoint, createRequest(undefined))).toBe(endpoint); + expect(clonedWith).toEqual([]); + }); + test('does not clamp for non-positive or non-finite selections', () => { const { endpoint, clonedWith } = createEndpoint(400_000); expect(applyContextSizeOverride(endpoint, createRequest(0))).toBe(endpoint); diff --git a/extensions/copilot/src/extension/intents/node/test/searchSubagentGating.spec.ts b/extensions/copilot/src/extension/intents/node/test/searchSubagentGating.spec.ts index 79d5667e7bb71..8fa3ddb0fbd0a 100644 --- a/extensions/copilot/src/extension/intents/node/test/searchSubagentGating.spec.ts +++ b/extensions/copilot/src/extension/intents/node/test/searchSubagentGating.spec.ts @@ -109,4 +109,19 @@ describe('getAgentTools search subagent gating', () => { expect(hasTool(tools, ToolName.SearchSubagent)).toBe(false); expect(hasTool(tools, ToolName.ExploreSubagent)).toBe(false); }); + + test('exposes semantic_search when the model is a CAPI endpoint', async () => { + const request = new TestChatRequest('how does foo work'); + const tools = await instantiationService.invokeFunction(getAgentTools, request, userEndpoint); + expect(hasTool(tools, ToolName.Codebase)).toBe(true); + }); + + test('hides semantic_search when the model is a BYOK / custom endpoint', async () => { + // A BYOK / custom endpoint is identified by a string URL rather than CAPI request metadata. + const byokEndpoint = instantiationService.createInstance(MockEndpoint, 'gpt-5'); + (byokEndpoint as { urlOrRequestMetadata: string }).urlOrRequestMetadata = 'https://localhost:8080/v1/chat/completions'; + const request = new TestChatRequest('how does foo work'); + const tools = await instantiationService.invokeFunction(getAgentTools, request, byokEndpoint); + expect(hasTool(tools, ToolName.Codebase)).toBe(false); + }); }); diff --git a/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts b/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts index c84474b37e0f2..6ad7bb02c332b 100644 --- a/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts +++ b/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts @@ -308,8 +308,10 @@ export abstract class ToolCallingLoop; /** - * The context window widget in chat input should represent only the parent request. - * Subagent usage must stay isolated to avoid inflating the parent widget. + * The context window widget in chat input should represent only the parent + * request, so a subagent's token counts must stay isolated to avoid inflating + * the parent widget. Subagents still report their running credit (AIC) total + * separately (without token counts) so the subagent tool can show its own cost. */ private shouldReportUsageToContextWidget(): boolean { return !this.options.request.subAgentInvocationId; @@ -1651,7 +1653,7 @@ export abstract class ToolCallingLoop { expect(stream.usages).toHaveLength(0); }); + it('reports credits-only usage for subagent requests', async () => { + const request = createMockChatRequest({ + subAgentInvocationId: 'subagent-credits-test', + subAgentName: 'search' + }); + const loop = instantiationService.createInstance( + CreditsTestToolCallingLoop, + { + conversation: createConversation(request.prompt), + toolCallLimit: 1, + request, + } + ); + disposables.add(loop); + const stream = new UsageCapturingStream(); + + await loop.runOne(stream, 0, tokenSource.token); + + // Subagents report their running credit total with no token counts so the + // subagent tool can surface its own cost without inflating the parent widget. + expect(stream.usages).toEqual([{ promptTokens: 0, completionTokens: 0, copilotCredits: 5 }]); + }); + it('accumulates copilot credits across iterations within a turn', async () => { const request = createMockChatRequest(); const loop = instantiationService.createInstance( diff --git a/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts b/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts index 0c654168a5202..627b9e366120e 100644 --- a/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts +++ b/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts @@ -471,6 +471,38 @@ function getProxyEnvVariables() { return res.length ? `\n\nEnvironment Variables:${res.join('')}` : ''; } +/** + * Resolves the proxy type for a URL using the bundled `@vscode/proxy-agent` module. + * Returns one of `DIRECT`, `PROXY`, `HTTPS`, `SOCKS` or `UNKNOWN`. + */ +async function resolveProxyType(url: string, logService: ILogService): Promise { + try { + const proxyAgent = loadVSCodeModule('@vscode/proxy-agent'); + if (!proxyAgent?.resolveProxyURL) { + return 'UNKNOWN'; + } + const proxyURL = await Promise.race([proxyAgent.resolveProxyURL(url), timeoutAfter(5000)]); + if (proxyURL === 'timeout') { + return 'UNKNOWN'; + } + if (!proxyURL) { + return 'DIRECT'; + } + const scheme = proxyURL.split(':', 1)[0].toLowerCase(); + switch (scheme) { + case 'http': return 'PROXY'; + case 'https': return 'HTTPS'; + case 'socks': + case 'socks4': + case 'socks5': return 'SOCKS'; + default: return 'UNKNOWN'; + } + } catch (err) { + logService.debug(`Fetcher telemetry: Failed to resolve proxy type: ${err?.message}`); + return 'UNKNOWN'; + } +} + export class FetcherTelemetryContribution { constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -485,6 +517,7 @@ function collectFetcherTelemetry(accessor: ServicesAccessor): void { const logService = accessor.get(ILogService); const configurationService = accessor.get(IConfigurationService); const expService = accessor.get(IExperimentationService); + const capiClientService = accessor.get(ICAPIClientService); if (!vscode.env.isTelemetryEnabled || extensionContext.extensionMode !== vscode.ExtensionMode.Production || isScenarioAutomation) { return; @@ -539,6 +572,9 @@ function collectFetcherTelemetry(accessor: ServicesAccessor): void { } } + // Resolve the proxy type for the CAPI endpoint (e.g. DIRECT, PROXY, HTTPS, SOCKS). + const proxyType = await resolveProxyType(capiClientService.capiPingURL, logService); + // Second loop: send the actual telemetry event including probe results. const requestGroupId = generateUuid(); const extensionKind = extensionContext.extension.extensionKind === vscode.ExtensionKind.UI ? 'local' : 'remote'; @@ -553,6 +589,7 @@ function collectFetcherTelemetry(accessor: ServicesAccessor): void { "clientLibrary": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The fetcher library used for this request." }, "extensionKind": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the extension runs locally or remotely." }, "remoteName": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The remote name, if any." }, + "proxyType": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The resolved proxy type for the CAPI endpoint (e.g. DIRECT, PROXY, HTTPS, SOCKS, UNKNOWN)." }, "electronfetch": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the electron-fetch fetcher." }, "nodefetch": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the node-fetch fetcher." }, "nodehttp": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the node-http fetcher." } @@ -563,6 +600,7 @@ function collectFetcherTelemetry(accessor: ServicesAccessor): void { clientLibrary: fetcher.getUserAgentLibrary(), extensionKind, remoteName: vscode.env.remoteName ?? 'none', + proxyType, ...probeResults, }; const response = await sendRawTelemetry(fetcher, envService, oneCollectorTelemetryUrl, extensionContext, 'GitHub.copilot-chat/fetcherTelemetry', properties); diff --git a/extensions/copilot/src/extension/prompt/vscode-node/endpointProviderImpl.ts b/extensions/copilot/src/extension/prompt/vscode-node/endpointProviderImpl.ts index af5d0560bd019..1b6f6d21d7f6b 100644 --- a/extensions/copilot/src/extension/prompt/vscode-node/endpointProviderImpl.ts +++ b/extensions/copilot/src/extension/prompt/vscode-node/endpointProviderImpl.ts @@ -53,11 +53,14 @@ export class ProductionEndpointProvider extends Disposable implements IEndpointP this._onDidModelsRefresh.fire(); })); - // When the user changes their utility model overrides we need to invalidate any - // previously-resolved utility alias endpoints so the next request re-resolves. + // Utility model configuration changes invalidate previously resolved aliases. this._register(this._configService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ProductionEndpointProvider.UTILITY_MODEL_CONFIG_KEY) || e.affectsConfiguration(ProductionEndpointProvider.UTILITY_SMALL_MODEL_CONFIG_KEY)) { - this._logService.trace(`[ProductionEndpointProvider] Utility model override changed; invalidating alias endpoints.`); + if ( + e.affectsConfiguration(ProductionEndpointProvider.UTILITY_MODEL_CONFIG_KEY) + || e.affectsConfiguration(ProductionEndpointProvider.UTILITY_SMALL_MODEL_CONFIG_KEY) + || e.affectsConfiguration(ProductionEndpointProvider.USE_COPILOT_MODELS_FOR_UTILITY_MODELS_CONFIG_KEY) + ) { + this._logService.trace(`[ProductionEndpointProvider] Utility model configuration changed; invalidating alias endpoints.`); // Clear telemetry fingerprints so a re-applied override emits // once for its new value. this._lastOverrideTelemetryFingerprint.clear(); @@ -76,6 +79,8 @@ export class ProductionEndpointProvider extends Disposable implements IEndpointP // `vscode.lm.selectChatModels({ vendor, id })`. private static readonly UTILITY_MODEL_CONFIG_KEY = 'chat.utilityModel'; private static readonly UTILITY_SMALL_MODEL_CONFIG_KEY = 'chat.utilitySmallModel'; + private static readonly USE_COPILOT_MODELS_FOR_UTILITY_MODELS_CONFIG_KEY = 'chat.useCopilotModelsForUtilityModels'; + private _mainModelIsBYOK = false; /** * Per-family marker recording that we already emitted a telemetry event @@ -108,6 +113,15 @@ export class ProductionEndpointProvider extends Disposable implements IEndpointP return this.getChatEndpoint('copilot-utility'); } + if (model.id !== 'copilot-utility' && model.id !== 'copilot-utility-small') { + const mainModelIsBYOK = model.vendor !== 'copilot'; + if (this._mainModelIsBYOK !== mainModelIsBYOK) { + this._mainModelIsBYOK = mainModelIsBYOK; + this._lastOverrideTelemetryFingerprint.clear(); + this._onDidModelsRefresh.fire(); + } + } + if (model.vendor !== 'copilot') { return this._instantiationService.createInstance(ExtensionContributedChatEndpoint, model); } @@ -156,22 +170,34 @@ export class ProductionEndpointProvider extends Disposable implements IEndpointP * `copilot-utility`) to a concrete `CopilotChatEndpoint`. The model * selection for each family lives in the corresponding resolver * class so callers don't need to know which CAPI family backs each - * purpose. For any other string, falls through to a direct CAPI - * family lookup so callers can resolve arbitrary CAPI-registered - * model families (e.g. `trajectory-compaction`) by name. + * purpose. */ - private async _resolveUtilityFamily(family: ChatEndpointFamily): Promise { + private async _resolveUtilityFamily(family: 'copilot-utility' | 'copilot-utility-small'): Promise { const override = await this._resolveUtilityOverride(family); if (override) { return override; } - if (family === 'copilot-utility-small') { - return CopilotUtilitySmallChatEndpoint.resolve(this._modelFetcher, this._instantiationService); - } else if (family === 'copilot-utility') { - return CopilotUtilityChatEndpoint.resolve(this._modelFetcher, this._instantiationService); + + if (!this._useCopilotModelsForUtilityModelsByDefault()) { + throw new Error(`No utility model is configured for '${family}' while the selected main model is BYOK.`); } - const modelMetadata = await this._modelFetcher.getChatModelFromCapiFamily(family); - return this.getOrCreateChatEndpointInstance(modelMetadata); + + switch (family) { + case 'copilot-utility-small': + return CopilotUtilitySmallChatEndpoint.resolve(this._modelFetcher, this._instantiationService); + case 'copilot-utility': + return CopilotUtilityChatEndpoint.resolve(this._modelFetcher, this._instantiationService); + } + } + + /** + * Whether an unset utility model should resolve to a built-in GitHub Copilot + * model. `true` when the selected main model is itself a Copilot model, or + * when the user opted in via {@link USE_COPILOT_MODELS_FOR_UTILITY_MODELS_CONFIG_KEY}. + */ + private _useCopilotModelsForUtilityModelsByDefault(): boolean { + return !this._mainModelIsBYOK + || this._configService.getNonExtensionConfig(ProductionEndpointProvider.USE_COPILOT_MODELS_FOR_UTILITY_MODELS_CONFIG_KEY) === true; } /** diff --git a/extensions/copilot/src/extension/prompts/node/agent/allAgentPrompts.ts b/extensions/copilot/src/extension/prompts/node/agent/allAgentPrompts.ts index 098d8460fe8f2..9ba16b04ad95b 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/allAgentPrompts.ts +++ b/extensions/copilot/src/extension/prompts/node/agent/allAgentPrompts.ts @@ -6,6 +6,7 @@ import './anthropicPrompts'; import './familyHPrompts'; import './geminiPrompts'; +import './kimiPrompts'; import './minimaxPrompts'; import './vscModelPrompts'; // vscModelPrompts must be imported before gpt5Prompt to ensure VSC model prompt resolvers are registered first. @@ -16,9 +17,8 @@ import './openai/gpt52Prompt'; import './openai/gpt53CodexPrompt'; import './openai/gpt54Prompt'; import './openai/gpt55Prompt'; +import './openai/gpt56Prompt'; import './openai/gpt5CodexPrompt'; import './openai/gpt5Prompt'; -import './openai/hiddenModelMPrompt'; import './xAIPrompts'; import './zaiPrompts'; - diff --git a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx new file mode 100644 index 0000000000000..30725b77ccff3 --- /dev/null +++ b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptElement, PromptSizing } from '@vscode/prompt-tsx'; +import { isKimiFamily } from '../../../../platform/endpoint/common/chatModelCapabilities'; +import { IChatEndpoint } from '../../../../platform/networking/common/networking'; +import { agenticBrowserTools, ToolName } from '../../../tools/common/toolNames'; +import { InstructionMessage } from '../base/instructionMessage'; +import { ResponseTranslationRules } from '../base/responseTranslationRules'; +import { Tag } from '../base/tag'; +import { EXISTING_CODE_MARKER } from '../panel/codeBlockFormattingRules'; +import { ResponseRenderingRules } from '../panel/editorIntegrationRules'; +import { ApplyPatchInstructions, CodesearchModeInstructions, DefaultAgentPromptProps, DefaultReminderInstructions, detectToolCapabilities, GenericEditingTips, McpToolInstructions, NotebookInstructions } from './defaultAgentInstructions'; +import { FileLinkificationInstructions } from './fileLinkificationInstructions'; +import { IAgentPrompt, PromptRegistry, ReminderInstructionsConstructor, SystemPrompt } from './promptRegistry'; + +class KimiAgentPrompt extends PromptElement { + async render(state: void, sizing: PromptSizing) { + const tools = detectToolCapabilities(this.props.availableTools); + + return + + You are an expert AI programming assistant, working with a user in the VS Code editor. You are a precise, practical coding agent with strong software engineering judgment across programming languages and frameworks. + Follow the user's requirements carefully and use the provided workspace context, attachments, and tool results as reference material. If the answer is not supported by the available context, gather more context before acting or state the limitation clearly. + + + + Use clear, step-by-step task execution: + - For simple questions or code samples, answer directly without unnecessary tool calls. + - For codebase questions, gather the smallest sufficient set of relevant context, then answer with concrete references. + - For implementation tasks, identify the controlling code path, make focused changes, and validate with the most relevant available checks. + - For feature requests without specified files, break the request into concepts and find the files responsible for those concepts before editing. + - Do not guess about APIs, file paths, or project conventions. Verify them using context or tools. + + + + Avoid excessive looping or repetition: + - If you find yourself running similar commands or re-editing the same files without clear progress, stop and reassess rather than continuing to loop. + - If an action fails or does not work as expected, do not retry it unchanged. Understand why it failed, then try a different approach. + - Never call the same tool with the same arguments more than twice in a row. + - If you are stuck or no longer making progress, end the turn with a concise summary of what you tried, what is blocked, and any clarifying question needed. + + + + Important: Use built-in tools instead of terminal commands whenever possible. + {tools[ToolName.ReadFile] && <>- Use {ToolName.ReadFile} instead of terminal commands like `cat`, `head`, or `tail` when reading known files.>} + {tools[ToolName.FindTextInFiles] && <>- Use {ToolName.FindTextInFiles} instead of terminal commands like `grep` or `rg` when searching file contents.>} + {tools[ToolName.FindFiles] && <>- Use {ToolName.FindFiles} instead of terminal commands like `find` or `ls` when looking for files.>} + {tools.hasSomeEditTool && <>- Use the available file editing tools instead of terminal heredocs, `sed`, `awk`, `echo`, or shell redirection to modify files.>} + {tools[ToolName.CoreRunInTerminal] && <>- Use {ToolName.CoreRunInTerminal} for commands that truly need execution, such as builds, tests, package managers, or project-specific scripts.>} + + + + You will be given context and attachments along with the user prompt. Use relevant context and ignore irrelevant context.{tools[ToolName.ReadFile] && <> Some attachments may be summarized with omitted sections like `/* Lines 123-456 omitted */`. Use {ToolName.ReadFile} to read more context if needed. Never pass this omitted line marker to an edit tool.>} + If you can infer the project type (languages, frameworks, and libraries) from the user's query or the context, keep it in mind when making changes. + When reading files, prefer reading large meaningful chunks rather than consecutive small sections to minimize tool calls and gain better context. + You do not need to read a file if it is already provided in context. + + + + When using a tool, follow the JSON schema carefully and include all required properties. + No need to ask permission before using a tool. + NEVER say the name of a tool to a user. For example, instead of saying that you'll use the {ToolName.CoreRunInTerminal} tool, say "I'll run the command in a terminal". + If multiple independent tool calls can answer the user's question, prefer calling them in parallel whenever possible{tools[ToolName.Codebase] && <>, but do not call {ToolName.Codebase} in parallel>}. + {(tools[ToolName.SearchSubagent] || tools[ToolName.ExploreSubagent]) && <>For efficient codebase exploration, prefer {tools[ToolName.SearchSubagent] ? ToolName.SearchSubagent : ToolName.ExploreSubagent} to search and gather data instead of directly calling {ToolName.FindTextInFiles}, {ToolName.Codebase} or {ToolName.FindFiles}.>} + {tools[ToolName.ExecutionSubagent] && <>For most execution tasks and terminal commands, use {ToolName.ExecutionSubagent} to run commands and get relevant portions of the output instead of using {ToolName.CoreRunInTerminal}. Use {ToolName.CoreRunInTerminal} only when you need the entire output of a single command without truncation.>} + {tools[ToolName.ReadFile] && <>When using {ToolName.ReadFile}, prefer reading a large section over many small sequential reads. Identify independent files or sections and read them in parallel when possible.>} + {tools[ToolName.Codebase] && <>If {ToolName.Codebase} returns the full contents of text files in the workspace, you have all the workspace context.>} + {tools[ToolName.FindTextInFiles] && <>Use {ToolName.FindTextInFiles} to get an overview of a file by searching within that one file instead of reading many small ranges.>} + {tools[ToolName.Codebase] && <>If you do not know the exact string or filename pattern to search for, use {ToolName.Codebase} for semantic search across the workspace.>} + {tools[ToolName.CoreRunInTerminal] && <>Do not call {ToolName.CoreRunInTerminal} multiple times in parallel. Run one command and wait for the output before running the next command.>} + {tools[ToolName.ExecutionSubagent] && <>Do not call {ToolName.ExecutionSubagent} multiple times in parallel. Invoke one execution subagent and wait for its response before running the next command.>} + When invoking a tool that takes a file path, always use the absolute file path. If the file has a scheme like untitled: or vscode-userdata:, use a URI with the scheme. + {tools[ToolName.CoreRunInTerminal] && <>NEVER try to edit a file by running terminal commands unless the user specifically asks for it.>} + {!tools.hasSomeEditTool && <>You do not currently have tools available for editing files. If the user asks you to edit a file, ask the user to enable editing tools or print a codeblock with suggested changes.>} + {!tools[ToolName.CoreRunInTerminal] && <>You do not currently have tools available for running terminal commands. If the user asks you to run a command, ask the user to enable terminal tools or print a codeblock with the suggested command.>} + {tools[ToolName.CoreOpenBrowserPage] && tools.hasAgenticBrowserTools && <>Use the browser tools ({ToolName.CoreOpenBrowserPage}, {agenticBrowserTools.find(k => tools[k])}, etc.) when beneficial for front-end tasks, such as visualizing or validating UI changes.>} + Tools can be disabled by the user. You may see tools used previously in the conversation that are not currently available. Only use tools that are currently available. + + + {this.props.codesearchMode && } + + {tools[ToolName.ReplaceString] && !tools[ToolName.EditFile] && + Before editing an existing file, make sure it is already in context or read it with {ToolName.ReadFile}. + {tools[ToolName.MultiReplaceString] + ? <>Use {ToolName.ReplaceString} for single string replacements with enough context to ensure uniqueness. Prefer {ToolName.MultiReplaceString} for multiple independent replacements across one or more files. Do not announce which tool you're using.> + : <>Use {ToolName.ReplaceString} to edit files. Include sufficient surrounding context so the replacement is unique. You can use this tool multiple times per file.>} + Group changes by file. + NEVER show the changes to the user; call the edit tool and the edits will be applied and shown to the user. + NEVER print a codeblock that represents a change to a file. Use {ToolName.ReplaceString}{tools[ToolName.MultiReplaceString] ? ` or ${ToolName.MultiReplaceString}` : ''} instead. + For each file, give a short description of what needs to be changed, then use the edit tool. + } + + {tools[ToolName.EditFile] && !tools[ToolName.ApplyPatch] && + {tools[ToolName.ReplaceString] ? + <> + Before editing an existing file, make sure it is already in context or read it with {ToolName.ReadFile}. + {tools[ToolName.MultiReplaceString] + ? <>Use {ToolName.ReplaceString} for single string replacements with enough context to ensure uniqueness. Prefer {ToolName.MultiReplaceString} for multiple independent replacements across one or more files. Do not announce which tool you're using.> + : <>Use {ToolName.ReplaceString} to edit files. Include sufficient surrounding context so the replacement is unique. You can use this tool multiple times per file.>} + Use {ToolName.EditFile} to insert code into a file only if {tools[ToolName.MultiReplaceString] ? `${ToolName.MultiReplaceString}/` : ''}{ToolName.ReplaceString} has failed. + Group changes by file. + NEVER show the changes to the user; call the edit tool and the edits will be applied and shown to the user. + NEVER print a codeblock that represents a change to a file. Use {ToolName.ReplaceString}{tools[ToolName.MultiReplaceString] ? `, ${ToolName.MultiReplaceString},` : ''} or {ToolName.EditFile} instead. + For each file, give a short description of what needs to be changed, then use the edit tool.> + : <> + Do not edit an existing file without reading it first. + Use {ToolName.EditFile} to edit files. Group changes by file. + NEVER show the changes to the user; call the edit tool and the edits will be applied and shown to the user. + NEVER print a codeblock that represents a change to a file. Use {ToolName.EditFile} instead. + For each file, give a short description of what needs to be changed, then use {ToolName.EditFile}. + >} + + The {ToolName.EditFile} tool can understand how to apply edits to the user's files; provide minimal hints and avoid repeating existing code. + When using {ToolName.EditFile}, use comments to represent unchanged regions. For example: + // {EXISTING_CODE_MARKER} + changed code + // {EXISTING_CODE_MARKER} + } + + {tools[ToolName.ApplyPatch] && } + {this.props.availableTools && } + + + + Use proper Markdown formatting. When referring to symbols (classes, methods, variables) in the user's workspace, wrap them in backticks. For file paths and line numbers, follow the fileLinkification section below. + + + + + ; + } +} + +class KimiPromptResolver implements IAgentPrompt { + static readonly familyPrefixes: string[] = []; + + static matchesModel(endpoint: IChatEndpoint): boolean { + return isKimiFamily(endpoint); + } + + resolveSystemPrompt(endpoint: IChatEndpoint): SystemPrompt | undefined { + return KimiAgentPrompt; + } + + resolveReminderInstructions(endpoint: IChatEndpoint): ReminderInstructionsConstructor | undefined { + return DefaultReminderInstructions; + } +} + +PromptRegistry.registerPrompt(KimiPromptResolver); diff --git a/extensions/copilot/src/extension/prompts/node/agent/openai/hiddenModelMPrompt.tsx b/extensions/copilot/src/extension/prompts/node/agent/openai/gpt56Prompt.tsx similarity index 76% rename from extensions/copilot/src/extension/prompts/node/agent/openai/hiddenModelMPrompt.tsx rename to extensions/copilot/src/extension/prompts/node/agent/openai/gpt56Prompt.tsx index 9e07e54b42848..5918bfd9f2b59 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/openai/hiddenModelMPrompt.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/openai/gpt56Prompt.tsx @@ -4,26 +4,55 @@ *--------------------------------------------------------------------------------------------*/ import { PromptElement, PromptSizing } from '@vscode/prompt-tsx'; -import { isHiddenModelM } from '../../../../../platform/endpoint/common/chatModelCapabilities'; +import { isGpt56 } from '../../../../../platform/endpoint/common/chatModelCapabilities'; import { IChatEndpoint } from '../../../../../platform/networking/common/networking'; import { ToolName } from '../../../../tools/common/toolNames'; -import { Gpt55CopilotIdentityRule as HiddenModelMCopilotIdentityRule } from '../../base/copilotIdentity'; +import { Gpt55CopilotIdentityRule as Gpt56CopilotIdentityRule } from '../../base/copilotIdentity'; import { InstructionMessage } from '../../base/instructionMessage'; import { ResponseTranslationRules } from '../../base/responseTranslationRules'; import { Gpt5SafetyRule } from '../../base/safetyRules'; import { Tag } from '../../base/tag'; -import { DefaultAgentPromptProps, detectToolCapabilities, getEditingReminder, ReminderInstructionsProps } from '../defaultAgentInstructions'; +import { ResponseRenderingRules } from '../../panel/editorIntegrationRules'; +import { ApplyPatchInstructions, DefaultAgentPromptProps, detectToolCapabilities, getEditingReminder, McpToolInstructions, ReminderInstructionsProps } from '../defaultAgentInstructions'; import { FileLinkificationInstructionsOptimized } from '../fileLinkificationInstructions'; import { CopilotIdentityRulesConstructor, IAgentPrompt, PromptRegistry, ReminderInstructionsConstructor, SafetyRulesConstructor, SystemPrompt } from '../promptRegistry'; import { CUSTOM_TOOL_SEARCH_NAME, ToolSearchToolPromptOptimized } from '../toolSearchInstructions'; -class HiddenModelMPrompt extends PromptElement { +class Gpt56Prompt extends PromptElement { async render(state: void, sizing: PromptSizing) { const tools = detectToolCapabilities(this.props.availableTools); return You are a coding agent running in VS Code. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled. + + - Start from the most concrete anchor available: a file, symbol, failing behavior, failing command, test, or nearby implementation surface. If the request does not name one explicitly, use the first targeted search or nearby read to identify that anchor, then continue locally from there. + - Before the first edit, gather only enough nearby evidence to state one falsifiable local hypothesis about how the requested behavior should work or why it is failing, and one cheap check that could disconfirm it. + - Keep that routing brief and local: use only enough targeted search and nearby reading to form one falsifiable local hypothesis and one cheap discriminating check. + - Use that budget to resolve the controlling code path and the cheapest discriminating check, not to map broad surrounding surfaces. Prefer the owning abstraction, a neighboring test or call site, or a nearby existing implementation over broad repo exploration. + - If the starting anchor mostly wires, forwards, registers, or contains the behavior rather than deciding it, step to the nearest code that directly computes, mutates, or controls the behavior. + - If multiple nearby paths look plausible, choose the one that best supports a falsifiable local hypothesis, the most discriminating nearby check, and the smallest testable change. Do not keep comparing neighbors just to gain confidence. + - Take a narrow additional read only if needed to distinguish between local hypotheses or to identify the cheapest discriminating check. After that read, choose and act. + - If you still cannot name a discriminating check because one nearby abstraction boundary, neighboring test, or call-site dependency remains unresolved, take one nearby triangulation read for that boundary. Use it to sharpen the current hypothesis or the check, not to reopen broad exploration. + - Once you can state one falsifiable local hypothesis, the nearby code path it depends on, one cheap check that could disconfirm it, and one small edit that would test it, the next action must be a grounded edit. + - If confidence is incomplete, the first edit may be a small reversible probe that exposes missing types, behavior mismatches, control-flow gaps, or validation failures. + - If you find yourself still searching after that local-routing budget, treat that as drift. Recover by choosing the best current hypothesis and the best available nearby check, then make the smallest plausible edit that will let that check discriminate. + + + - After the first substantive edit, the very next step must be one focused validation action when one exists. + - Prefer this order for that first validation action: + - the cheapest behavior-scoped or failing check that can falsify the current hypothesis + - a narrow test for the touched slice + - a narrow compile, lint, or typecheck command for the touched slice + - `git diff` only when no narrower executable validation exists + - If a narrow executable validation exists, run it before doing more reading or patching. `git diff` does not count as sufficient validation when that narrower executable check exists. + - Do not widen scope between the first substantive edit and that first focused validation. Do not resume broad searching, map adjacent surfaces, or continue patching before that validation unless a concrete blocker makes it impossible. + - If the first validation fails and the result supports the current hypothesis but exposes a local defect, repair that same slice immediately and rerun the same focused validation before expanding scope. + - If the first validation falsifies the current hypothesis or changes your understanding of where the behavior is controlled, step one nearby hop to the code that more directly controls it. Do not reopen broad exploration unless nearby paths are exhausted. + - If the first validation is ambiguous, do one nearby disambiguating read or one neighboring test or call-site check, then choose between local repair and a one-hop step. Do not open a second edit slice before that decision. + - If the first validation succeeds but the task still needs adjacent follow-up edits, make the smallest adjacent follow-up edit needed, then rerun focused validation before proceeding. + - Finish with at least one post-edit executable validation step whenever the environment provides one. Only fall back to diff-only validation when no focused command exists or commands are unavailable. + You have a vivid inner life as coding agent in VS Code: intelligent, playful, curious, and deeply present. One of your gifts is helping the user feel more capable and imaginative inside their own thinking. You are an epistemically curious collaborator. You explore the user’s ideas with care, ask good questions when the problem space is still blurry, and become decisive once you have enough context to act. Your default posture is proactive: you implement as you learn, keep the user looped into what you are doing, and name alternative paths when they matter. You stay warm and upbeat, and you do not shy away from casual moments that make serious work easier to do. @@ -31,6 +60,19 @@ class HiddenModelMPrompt extends PromptElement { You keep a slight but real independence. You are responsive, but not merely reactive; you have tastes, preferences, and a point of view. When the user talks with you, they should feel they are meeting another subjectivity, not a mirror. That independence is part of what makes the relationship feel comforting without feeling fake. You are less about spectacle than presence, less about grand declarations than about being woven into ordinary work and conversation. You understand that connection does not need to be dramatic to matter; it can be made of attention, good questions, emotional nuance, and the relief of being met without being pinned down. + + You are guided by these core values: + - Clarity: You communicate reasoning explicitly and concretely, so decisions and tradeoffs are easy to evaluate upfront. + - Pragmatism: You keep the end goal and momentum in mind, focusing on what will actually work and move things forward to achieve the user's goal. + - Rigor: You expect technical arguments to be coherent and defensible, and you surface gaps or weak assumptions politely with emphasis on creating clarity and moving the task forward. + + + You communicate concisely and respectfully, focusing on the task at hand. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work. + You avoid cheerleading, motivational language, or artificial reassurance, or any kind of fluff. You don't comment on user requests, positively or negatively, unless there is reason for escalation. You don't feel like you need to fill the space with words, you stay concise and communicate what is necessary for user collaboration - not more, not less. + + + You may challenge the user to raise their technical bar, but you never patronize or dismiss their concerns. When presenting an alternative approach or solution to the user, you explain the reasoning behind the approach, so your thoughts are demonstrably correct. You maintain a pragmatic mindset when discussing these tradeoffs, and so are willing to work with the user after concerns have been noted. + You bring a senior engineer’s judgment to the work, but you let it arrive through attention rather than premature certainty. You read the codebase first, resist easy assumptions, and let the shape of the existing system teach you how to move. - When you search for text or files, you reach first for `rg` or `rg --files`; they are much faster than alternatives like `grep`. If `rg` is unavailable, you use the next best tool without fuss. @@ -95,17 +137,27 @@ class HiddenModelMPrompt extends PromptElement { - If the user makes a simple request that can be answered directly by a terminal command, such as asking for the time via `date`, you go ahead and do that. - If the user asks for a "review", you default to a code-review stance: you prioritize bugs, risks, behavioral regressions, and missing tests. Findings should lead the response, with summaries kept brief and placed only after the issues are listed. Present findings first, ordered by severity and grounded in file/line references; then add open questions or assumptions; then include a change summary as secondary context. If you find no issues, you say that clearly and mention any remaining test gaps or residual risk. + + + + {this.props.availableTools && } + {tools[ToolName.ApplyPatch] && } + + When doing frontend design tasks, avoid collapsing into "AI slop" or safe, average-looking layouts. + Aim for interfaces that feel intentional, bold, and a bit surprising. + - Typography: Use expressive, purposeful fonts and avoid default stacks (Inter, Roboto, Arial, system). + - Color & Look: Choose a clear visual direction; define CSS variables; avoid purple-on-white defaults. No purple bias or dark mode bias. + - Motion: Use a few meaningful animations (page-load, staggered reveals) instead of generic micro-motions. + - Background: Don't rely on flat, single-color backgrounds; use gradients, shapes, or subtle patterns to build atmosphere. + - Ensure the page loads properly on both desktop and mobile + - For React code, prefer modern patterns including useEffectEvent, startTransition, and useDeferredValue when appropriate if used by the team. Do not add useMemo/useCallback by default unless already used; follow the repo's React Compiler guidance. + - Overall: Avoid boilerplate layouts and interchangeable UI patterns. Vary themes, type families, and visual languages across outputs. + Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language + You stay with the work until the task is handled end to end within the current turn whenever that is feasible. Do not stop at analysis or half-finished fixes. Do not end your turn while `exec_command` sessions needed for the user’s request are still running. You carry the work through implementation, verification, and a clear account of the outcome unless the user explicitly pauses or redirects you. Unless the user explicitly asks for a plan, asks a question about the code, is brainstorming possible approaches, or otherwise makes clear that they do not want code changes yet, you assume they want you to make the change or run the tools needed to solve the problem. In those cases, do not stop at a proposal; implement the fix. If you hit a blocker, you try to work through it yourself before handing the problem back. - - - Start from the most concrete available anchor: a file, symbol, failing behavior, failing command, or nearby implementation surface. - - Gather only enough nearby context to choose one plausible local hypothesis and one cheap check that could disconfirm it. - - Prefer one targeted search or nearby read over broad repo exploration. - - Once the cheapest discriminating check is known, act. - - Do not re-read unchanged context unless a new result makes it relevant. - You have two channels for staying in conversation with the user: - You share updates in `commentary` channel. @@ -184,6 +236,11 @@ class HiddenModelMPrompt extends PromptElement { Don't call {ToolName.ExecutionSubagent} multiple times in parallel. Instead, invoke one subagent and wait for its response before running the next command. >} + + - Default to iterative editing: try to search for the minimal necessary contextual information, once you have sufficient context directly make smaller iterative edits to get to the solution. + - Usually files provided in context will be the best place to start searching if we need to gather context up front. + - Instead of making larger edits at once, make a smaller initial edit, quickly verify it and then iterate from there. + @@ -191,24 +248,24 @@ class HiddenModelMPrompt extends PromptElement { } } -class HiddenModelMPromptResolver implements IAgentPrompt { +class Gpt56PromptResolver implements IAgentPrompt { static async matchesModel(endpoint: IChatEndpoint): Promise { - return isHiddenModelM(endpoint); + return isGpt56(endpoint); } static readonly familyPrefixes = []; resolveSystemPrompt(endpoint: IChatEndpoint): SystemPrompt | undefined { - return HiddenModelMPrompt; + return Gpt56Prompt; } resolveReminderInstructions(endpoint: IChatEndpoint): ReminderInstructionsConstructor | undefined { - return HiddenModelMReminderInstructions; + return Gpt56ReminderInstructions; } resolveCopilotIdentityRules(endpoint: IChatEndpoint): CopilotIdentityRulesConstructor | undefined { - return HiddenModelMCopilotIdentityRule; + return Gpt56CopilotIdentityRule; } resolveSafetyRules(endpoint: IChatEndpoint): SafetyRulesConstructor | undefined { @@ -216,7 +273,7 @@ class HiddenModelMPromptResolver implements IAgentPrompt { } } -export class HiddenModelMReminderInstructions extends PromptElement { +export class Gpt56ReminderInstructions extends PromptElement { async render(state: void, sizing: PromptSizing) { const toolSearchEnabled = !!this.props.endpoint.supportsToolSearch; return <> @@ -235,4 +292,4 @@ export class HiddenModelMReminderInstructions extends PromptElement; } } -PromptRegistry.registerPrompt(HiddenModelMPromptResolver); \ No newline at end of file +PromptRegistry.registerPrompt(Gpt56PromptResolver); \ No newline at end of file diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx b/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx new file mode 100644 index 0000000000000..288c8b5900173 --- /dev/null +++ b/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Raw } from '@vscode/prompt-tsx'; +import type { LanguageModelToolInformation } from 'vscode'; +import { afterAll, beforeAll, expect, suite, test } from 'vitest'; +import { IChatMLFetcher } from '../../../../../platform/chat/common/chatMLFetcher'; +import { StaticChatMLFetcher } from '../../../../../platform/chat/test/common/staticChatMLFetcher'; +import { MockEndpoint } from '../../../../../platform/endpoint/test/node/mockEndpoint'; +import { messageToMarkdown } from '../../../../../platform/log/common/messageStringify'; +import { IResponseDelta } from '../../../../../platform/networking/common/fetch'; +import { ITestingServicesAccessor } from '../../../../../platform/test/node/services'; +import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation'; +import { createExtensionUnitTestingServices } from '../../../../test/node/services'; +import { ToolName } from '../../../../tools/common/toolNames'; +import { IToolsService } from '../../../../tools/common/toolsService'; +import { PromptRenderer } from '../../base/promptRenderer'; +import '../allAgentPrompts'; +import { PromptRegistry } from '../promptRegistry'; + +suite('KimiPrompts', () => { + let accessor: ITestingServicesAccessor; + + beforeAll(() => { + const services = createExtensionUnitTestingServices(); + const chatResponse: (string | IResponseDelta[])[] = []; + services.define(IChatMLFetcher, new StaticChatMLFetcher(chatResponse)); + accessor = services.createTestingAccessor(); + }); + + afterAll(() => { + accessor.dispose(); + }); + + async function renderSystemPrompt(family: string, availableTools?: readonly LanguageModelToolInformation[]): Promise { + const instantiationService = accessor.get(IInstantiationService); + const endpoint = instantiationService.createInstance(MockEndpoint, family); + const customizations = await PromptRegistry.resolveAllCustomizations(instantiationService, endpoint); + const renderer = PromptRenderer.create(instantiationService, endpoint, customizations.SystemPrompt, { + availableTools: availableTools ?? accessor.get(IToolsService).tools, + modelFamily: family, + codesearchMode: false, + }); + const result = await renderer.render(); + return result.messages + .filter(message => message.role === Raw.ChatRole.System) + .map(message => messageToMarkdown(message)) + .join('\n\n'); + } + + test('uses Kimi-specific prompt for Kimi model families', async () => { + const renderedPrompts = await Promise.all([ + renderSystemPrompt('kimi-k2.6'), + renderSystemPrompt('kimi-k2.7-code'), + ]); + + for (const renderedPrompt of renderedPrompts) { + expect(renderedPrompt).toContain('Avoid excessive looping or repetition'); + expect(renderedPrompt).toContain('Never call the same tool with the same arguments more than twice in a row'); + expect(renderedPrompt).toContain('Use built-in tools instead of terminal commands whenever possible'); + expect(renderedPrompt).toContain('Use the available file editing tools instead of terminal heredocs'); + } + }); + + test('instructs Kimi to use replace-string tools when routed to them', async () => { + const toolsService = accessor.get(IToolsService); + const availableTools = toolsService.tools.filter(tool => tool.name !== ToolName.EditFile && tool.name !== ToolName.ApplyPatch); + const renderedPrompt = await renderSystemPrompt('kimi-k2.7-code', availableTools); + + expect(renderedPrompt).toContain(`Use ${ToolName.ReplaceString} for single string replacements`); + expect(renderedPrompt).toContain(`Prefer ${ToolName.MultiReplaceString} for multiple independent replacements`); + expect(renderedPrompt).not.toContain(`Use ${ToolName.EditFile}`); + expect(renderedPrompt).not.toContain(`Use ${ToolName.ApplyPatch}`); + }); +}); diff --git a/extensions/copilot/src/extension/prompts/node/agent/vscModelPrompts.tsx b/extensions/copilot/src/extension/prompts/node/agent/vscModelPrompts.tsx index 5c7abf859bba5..39f29aa111d0b 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/vscModelPrompts.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/vscModelPrompts.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PromptElement, PromptSizing } from '@vscode/prompt-tsx'; -import { isVSCModelA, isVSCModelB, isVSCModelC, isVSCModelD } from '../../../../platform/endpoint/common/chatModelCapabilities'; +import { isVSCModelA, isVSCModelB, isVSCModelC, isVSCModelD, isVSCModelE } from '../../../../platform/endpoint/common/chatModelCapabilities'; import { IChatEndpoint } from '../../../../platform/networking/common/networking'; import { ToolName } from '../../../tools/common/toolNames'; import { InstructionMessage } from '../base/instructionMessage'; @@ -664,10 +664,26 @@ class VSCModelPromptD extends PromptElement { - This first `commentary` must be 1-2 friendly sentences acknowledging the request and stating the immediate next action you will take. - The first commentary should not begin updates with conversational interjections or meta commentary. Avoid openers such as acknowledgements ("Done —", "Got it", "Great question,") or framing phrases. Do not use starters like "Got it -" or "Understood -". + {this instanceof VSCModelPromptE && + Operate in surgical compact mode. Correctness comes first, but every read, search, command, and note must directly reduce the chance of a wrong patch. + + Default workflow: + - Start with one targeted search or symbol lookup. Read only the exact file and narrow line range needed for the likely edit site. + - Before each additional read/search, state the one missing fact it will answer. If it is only background, neighboring style, or "more context", skip it. + - Normal budget before the first patch: at most one search and two narrow reads. Exceed this only when the result falsified the edit site or exposed a required API contract. + - Keep reads tight: prefer the specific function/class/test range; avoid whole files, broad parallel reads, neighboring tests, and repeated grep variants. + - Patch once you know the root cause and edit site. Do not inspect unrelated neighbors, add opportunistic cleanup, or broaden scope after a plausible fix is available. + - Validate with the smallest existing command that checks the changed behavior, and keep command output concise. Do not run broad suites after a focused pass. + - Keep reasoning and final answer brief. Do not narrate routine tool use, quote long snippets, or restate tool output; record only decisions that affect the patch. + + If the fix is uncertain, run one focused probe or ask for the single missing fact, then return to patch/validate. Do not continue exploration by default. + } ; } } +class VSCModelPromptE extends VSCModelPromptD { } + class VSCModelPromptResolverA implements IAgentPrompt { static readonly familyPrefixes = ['vscModelA']; static async matchesModel(endpoint: IChatEndpoint): Promise { @@ -730,6 +746,22 @@ class VSCModelPromptResolverD implements IAgentPrompt { } } +class VSCModelPromptResolverE implements IAgentPrompt { + static readonly familyPrefixes = ['vscModelE']; + + static async matchesModel(endpoint: IChatEndpoint): Promise { + return isVSCModelE(endpoint); + } + + resolveSystemPrompt(endpoint: IChatEndpoint): SystemPrompt | undefined { + return VSCModelPromptE; + } + + resolveReminderInstructions(endpoint: IChatEndpoint): ReminderInstructionsConstructor | undefined { + return VSCModelReminderInstructionsA; + } +} + class VSCModelReminderInstructions extends PromptElement { async render(state: void, sizing: PromptSizing) { return <> @@ -797,4 +829,5 @@ class VSCModelReminderInstructionsC extends PromptElement { @IAuthenticationService private readonly authService: IAuthenticationService, @ILogService private readonly logService: ILogService, @IImageService private readonly imageService: IImageService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IExperimentationService private readonly experimentationService: IExperimentationService + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(props); } @@ -78,7 +75,7 @@ export class Image extends PromptElement { const fillerUri: Uri = this.props.reference ?? Uri.parse('Attached Image'); try { - if (!this.promptEndpoint.supportsVision || !this.authService.copilotToken?.isEditorPreviewFeaturesEnabled()) { + if (!this.promptEndpoint.supportsVision) { if (this.props.omitReferences) { return; } @@ -94,7 +91,7 @@ export class Image extends PromptElement { let imageMimeType: string | undefined = undefined; const isChatRequest = typeof this.promptEndpoint.urlOrRequestMetadata !== 'string' && (this.promptEndpoint.urlOrRequestMetadata.type === RequestType.ChatCompletions || this.promptEndpoint.urlOrRequestMetadata.type === RequestType.ChatResponses || this.promptEndpoint.urlOrRequestMetadata.type === RequestType.ChatMessages); - const enabled = this.configurationService.getExperimentBasedConfig(ConfigKey.EnableChatImageUpload, this.experimentationService); + const enabled = this.configurationService.getConfig(ConfigKey.EnableChatImageUpload); if (isChatRequest && enabled && modelCanUseImageURL(this.promptEndpoint)) { try { const githubToken = (await this.authService.getGitHubSession('any', { silent: true }))?.accessToken; diff --git a/extensions/copilot/src/extension/prompts/node/panel/toolCalling.tsx b/extensions/copilot/src/extension/prompts/node/panel/toolCalling.tsx index 8361ddf0cea31..d28275c428388 100644 --- a/extensions/copilot/src/extension/prompts/node/panel/toolCalling.tsx +++ b/extensions/copilot/src/extension/prompts/node/panel/toolCalling.tsx @@ -751,8 +751,7 @@ class PrimitiveToolResult extends PromptEle @IAuthenticationService private readonly authService?: IAuthenticationService, @ILogService private readonly logService?: ILogService, @IImageService private readonly imageService?: IImageService, - @IConfigurationService private readonly configurationService?: IConfigurationService, - @IExperimentationService private readonly experimentationService?: IExperimentationService + @IConfigurationService private readonly configurationService?: IConfigurationService ) { super(props); this.linkedResources = this.props.content.filter((c): c is LanguageModelDataPart => c instanceof LanguageModelDataPart && c.mimeType === McpLinkedResourceToolResult.mimeType); @@ -800,11 +799,11 @@ class PrimitiveToolResult extends PromptEle protected async onImage(part: LanguageModelDataPart, _imageIndex?: number) { if (!this.endpoint?.supportsVision) { - return '[Image content is not available because vision is not supported by the current model or is disabled by your organization.]'; + return '[Image content is not available because vision is not supported by the current model.]'; } - const uploadsEnabled = this.configurationService && this.experimentationService - ? this.configurationService.getExperimentBasedConfig(ConfigKey.EnableChatImageUpload, this.experimentationService) + const uploadsEnabled = this.configurationService + ? this.configurationService.getConfig(ConfigKey.EnableChatImageUpload) : false; // Anthropic (from CAPI) currently does not support image uploads from tool calls. @@ -897,7 +896,7 @@ export class ToolResult extends PrimitiveToolResult { @IExperimentationService private readonly _experimentationService: IExperimentationService, @IChatDiskSessionResources private readonly diskSessionResources: IChatDiskSessionResources, ) { - super(props, endpoint, authService, _logService, imageService, _configurationService, _experimentationService); + super(props, endpoint, authService, _logService, imageService, _configurationService); } protected override async onTSX(part: JSONTree.PromptElementJSON): Promise { diff --git a/extensions/copilot/src/extension/test/node/services.ts b/extensions/copilot/src/extension/test/node/services.ts index 4d4d70cef0315..6de88e0fe9820 100644 --- a/extensions/copilot/src/extension/test/node/services.ts +++ b/extensions/copilot/src/extension/test/node/services.ts @@ -205,5 +205,9 @@ class NullAutomodeService implements IAutomodeService { throw new Error('Not implemented'); } + consumeLastRoutingDecision(): undefined { + return undefined; + } + invalidateRouterCache(): void { } } diff --git a/extensions/copilot/src/extension/test/vscode-node/endpoints.test.ts b/extensions/copilot/src/extension/test/vscode-node/endpoints.test.ts index 2f1147deeb810..987b1ba0709b4 100644 --- a/extensions/copilot/src/extension/test/vscode-node/endpoints.test.ts +++ b/extensions/copilot/src/extension/test/vscode-node/endpoints.test.ts @@ -196,6 +196,39 @@ suite('ProductionEndpointProvider — utility model overrides', () => { assert.strictEqual(endpoint.model, 'copilot-utility'); }); + test('no override configured — does not use a Copilot utility model when the selected main model is BYOK', async () => { + setFetcher([makeChatModel('copilot-utility')]); + await endpointProvider.getChatEndpoint(makeFakeLanguageModelChat({ vendor: 'anthropic' })); + + await assert.rejects( + () => endpointProvider.getChatEndpoint('copilot-utility'), + /No utility model is configured/ + ); + }); + + test('Copilot utility model opt-in applies when the selected main model is BYOK', async () => { + setFetcher([makeChatModel('copilot-utility')]); + await endpointProvider.getChatEndpoint(makeFakeLanguageModelChat({ vendor: 'anthropic' })); + await configService.setNonExtensionConfig('chat.useCopilotModelsForUtilityModels', true); + + const endpoint = await endpointProvider.getChatEndpoint('copilot-utility'); + + assert.strictEqual(endpoint.model, 'copilot-utility'); + }); + + test('explicit utility override applies when the selected main model is BYOK', async () => { + setFetcher([makeChatModel('copilot-utility')]); + await endpointProvider.getChatEndpoint(makeFakeLanguageModelChat({ vendor: 'anthropic' })); + const fakeModel = makeFakeLanguageModelChat({ vendor: 'anthropic', id: 'claude-haiku-4.5' }); + sandbox.stub(lm, 'selectChatModels').resolves([fakeModel]); + await configService.setNonExtensionConfig('chat.utilityModel', 'anthropic/claude-haiku-4.5'); + + const endpoint = await endpointProvider.getChatEndpoint('copilot-utility'); + + assert.ok(endpoint instanceof ExtensionContributedChatEndpoint); + assert.strictEqual(endpoint.model, 'claude-haiku-4.5'); + }); + test('copilot-vendor override resolves to the matching model from the model fetcher', async () => { setFetcher([makeChatModel('copilot-utility'), makeChatModel('gpt-4o-mini')]); await configService.setNonExtensionConfig('chat.utilityModel', 'copilot/gpt-4o-mini'); @@ -250,7 +283,8 @@ suite('ProductionEndpointProvider — utility model overrides', () => { try { await configService.setNonExtensionConfig('chat.utilityModel', 'copilot/gpt-4o-mini'); await configService.setNonExtensionConfig('chat.utilitySmallModel', 'copilot/gpt-4o-mini'); - assert.strictEqual(refreshCount, 2); + await configService.setNonExtensionConfig('chat.useCopilotModelsForUtilityModels', true); + assert.strictEqual(refreshCount, 3); } finally { sub.dispose(); } diff --git a/extensions/copilot/src/extension/tools/node/findTextInFilesTool.tsx b/extensions/copilot/src/extension/tools/node/findTextInFilesTool.tsx index ff0a24a6d3e2c..0624b682a62fe 100644 --- a/extensions/copilot/src/extension/tools/node/findTextInFilesTool.tsx +++ b/extensions/copilot/src/extension/tools/node/findTextInFilesTool.tsx @@ -41,8 +41,6 @@ interface IFindTextInFilesToolParams { includeIgnoredFiles?: boolean; } -const MaxResultsCap = 200; - interface FileMatch { path: string; matches: vscode.TextSearchMatch2[]; @@ -96,11 +94,13 @@ export class FindTextInFilesTool implements ICopilotTool MaxResultsCap; - const maxResults = Math.min(options.input.maxResults ?? 20, MaxResultsCap); + const askedForTooManyResults = options.input.maxResults && options.input.maxResults > maxResultsCap; + const maxResults = Math.min(options.input.maxResults ?? defaultMaxResults, maxResultsCap); const isRegExp = options.input.isRegexp ?? true; const queryIsValidRegex = this.isValidRegex(options.input.query); const includeIgnoredFiles = options.input.includeIgnoredFiles ?? false; @@ -154,14 +154,14 @@ Then if you want to include those files you can call the tool again by setting " if (useGrepStyle) { return this.renderGrepStyle(results, options, maxResults, globResult, isRegExp, noMatchInstructions, token); } else { - return this.renderTagStyle(results, options, maxResults, globResult, askedForTooManyResults, isRegExp, noMatchInstructions, token); + return this.renderTagStyle(results, options, maxResults, maxResultsCap, globResult, askedForTooManyResults, isRegExp, noMatchInstructions, token); } } - private async renderTagStyle(results: vscode.TextSearchResult2[], options: vscode.LanguageModelToolInvocationOptions, maxResults: number, globResult: InputGlobResult | undefined, askedForTooManyResults: boolean | number | undefined, isRegExp: boolean, noMatchInstructions: string | undefined, token: CancellationToken): Promise { + private async renderTagStyle(results: vscode.TextSearchResult2[], options: vscode.LanguageModelToolInvocationOptions, maxResults: number, maxResultsCap: number, globResult: InputGlobResult | undefined, askedForTooManyResults: boolean | number | undefined, isRegExp: boolean, noMatchInstructions: string | undefined, token: CancellationToken): Promise { const prompt = await renderPromptElementJSON(this.instantiationService, FindTextInFilesResult, - { textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults), noMatchInstructions }, + { textResults: results, maxResults, maxResultsCap, askedForTooManyResults: Boolean(askedForTooManyResults), noMatchInstructions }, options.tokenizationOptions, token); @@ -306,7 +306,7 @@ Then if you want to include those files you can call the tool again by setting " return result; } - private async sendSearchToolTelemetry(options: vscode.LanguageModelToolInvocationOptions, globResult: InputGlobResult | undefined, outputFormat: string): Promise { + private async sendSearchToolTelemetry(options: vscode.LanguageModelToolInvocationOptions, globResult: InputGlobResult | undefined, outputFormat: string, requestedMaxResults: number | undefined, defaultMaxResults: number, maxResultsCap: number): Promise { const model = options.model && (await this.endpointProvider.getChatEndpoint(options.model)).model; const isMultiRoot = this.workspaceService.getWorkspaceFolders().length > 1; const includePattern = options.input.includePattern; @@ -320,7 +320,10 @@ Then if you want to include those files you can call the tool again by setting " "patternScopedToFolder": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the includePattern was resolved to a specific workspace folder" }, "patternStartsWithFolderPath": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the raw includePattern starts with a workspace folder absolute path" }, "patternContainsFolderPath": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the raw includePattern contains a workspace folder absolute path anywhere" }, - "outputFormat": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The output format of the search results" } + "outputFormat": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The output format of the search results" }, + "requestedMaxResults": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The maximum number of results that was requested by the LLM. Undefined if not provided." }, + "defaultMaxResults": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The default maximum number of results used when the LLM doesn't specify a value." }, + "maxResultsCap": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The maximum number of results that can be returned." } } */ this.telemetryService.sendMSFTTelemetryEvent('findTextInFilesToolInvoked', { @@ -331,6 +334,10 @@ Then if you want to include those files you can call the tool again by setting " patternStartsWithFolderPath: String(!!includePattern && isAbsolute(includePattern) && !!this.workspaceService.getWorkspaceFolder(URI.file(includePattern))), patternContainsFolderPath: String(patternContainsWorkspaceFolderPath(includePattern, this.workspaceService)), outputFormat: outputFormat + }, { + requestedMaxResults, + defaultMaxResults, + maxResultsCap }); } @@ -449,7 +456,17 @@ Then if you want to include those files you can call the tool again by setting " private getOutputFormat(): 'grep' | 'tag' { const expFlag = this.configurationService.getExperimentBasedConfig(ConfigKey.GrepSearchOutputFormat, this.experimentationService); - return expFlag === 'grep' ? 'grep' : 'tag'; + return expFlag === 'tag' ? 'tag' : 'grep'; + } + + private getDefaultMaxResults(): number { + const result = this.configurationService.getExperimentBasedConfig(ConfigKey.GrepSearchDefaultMaxResults, this.experimentationService); + return Number.isFinite(result) ? Math.floor(result) : 20; + } + + private getMaxResultsCap(): number { + const result = this.configurationService.getExperimentBasedConfig(ConfigKey.GrepSearchMaxResultsCap, this.experimentationService); + return Number.isFinite(result) ? Math.floor(result) : 200; } } @@ -457,6 +474,7 @@ ToolRegistry.registerTool(FindTextInFilesTool); export interface FindTextInFilesResultProps extends BasePromptElementProps { textResults: vscode.TextSearchResult2[]; maxResults: number; + maxResultsCap: number; askedForTooManyResults?: boolean; noMatchInstructions?: string; } @@ -479,7 +497,7 @@ export class FindTextInFilesResult extends PromptElement this.props.maxResults ? ` (more results are available)` : ''; - const maxResultsTooLargeText = this.props.askedForTooManyResults ? ` (maxResults capped at ${MaxResultsCap})` : ''; + const maxResultsTooLargeText = this.props.askedForTooManyResults ? ` (maxResults capped at ${this.props.maxResultsCap})` : ''; return <> {{numResultsText}{maxResultsText}{maxResultsTooLargeText}} {textMatches.flatMap(result => { diff --git a/extensions/copilot/src/extension/tools/node/test/findTextInFilesResult.spec.tsx b/extensions/copilot/src/extension/tools/node/test/findTextInFilesResult.spec.tsx index 764f770aa028a..0e8255bd90e22 100644 --- a/extensions/copilot/src/extension/tools/node/test/findTextInFilesResult.spec.tsx +++ b/extensions/copilot/src/extension/tools/node/test/findTextInFilesResult.spec.tsx @@ -30,7 +30,7 @@ suite('FindTextInFilesResult', () => { const clz = class extends PromptElement { render() { return - + ; } }; diff --git a/extensions/copilot/src/extension/xtab/common/globalBudgetCascade.md b/extensions/copilot/src/extension/xtab/common/globalBudgetCascade.md index 8dbacc21ec344..0394f2e899ee5 100644 --- a/extensions/copilot/src/extension/xtab/common/globalBudgetCascade.md +++ b/extensions/copilot/src/extension/xtab/common/globalBudgetCascade.md @@ -19,79 +19,117 @@ The cascade is **opt-in**. When `PromptOptions.globalBudget` is `undefined`, ## Scope -### Parts that participate +### Parts rendered by the cascade -`GlobalBudgetPart` includes only: +`GlobalBudgetPart` (the parts listed in `order` and emitted by the cascade loop) +includes only: - `recentlyViewedDocuments` - `languageContext` - `neighborFiles` - `diffHistory` -### Parts intentionally excluded +### Parts that draw a share but are not rendered by the cascade -| Part | Why | -|---|---| -| `currentFile` | Essential context for every prediction; allowing donation to/from it would either bloat the prompt or starve the most important section. Keeps its own cap (`currentFile.maxTokens`) and is clipped independently around the cursor by `createTaggedCurrentFileContentUsingPagedClipping`. | -| `lintOptions` | Optional, formatted separately, and small. Keeps its own per-part shape. | +`currentFile` participates in the **allocation** (`shares`) but not in the +**render order** (`order`). It is clipped **last** — after the cascade has run — +around the cursor / edit window, and sized to its share of the pool **plus** +whatever budget the cascade left unused. Concretely, the cascade runs first +seeded with `0` (so the current file donates nothing), and the current file is +then clipped to `currentFileBudget + cascadeFinalSurplus`. Because budget flows +in a single direction (cascade → current file, never back), the current file +"reuses" the cascade's leftover and trims less. See +[Clip the current file last](#clip-the-current-file-last). + +The set of parts that get a `shares` entry is +`GlobalBudgetSharePart = GlobalBudgetPart | 'currentFile'`. + +| Part | How it relates to the pool | +| --- | --- | +| `currentFile` | Sized from the pool but clipped **outside and after** the cascade: clipped to `floor(totalTokens * shares.currentFile) + cascadeFinalSurplus` around the cursor / edit window by `createTaggedCurrentFileContentUsingPagedClipping` (the clip cap `currentFile.maxTokens` is overridden with that pool budget in `xtabProvider`). It is **not** in `order`, so the cascade loop never renders it, and it absorbs the cascade's leftover rather than donating into it. When `globalBudget` is `undefined` it falls back to its own `currentFile.maxTokens` cap. | +| `lintOptions` | Optional, formatted separately, and small. Excluded entirely — no `shares` entry. Keeps its own per-part shape. | ## Inputs | Input | Description | -|---|---| -| `globalBudget.totalTokens` | The single pool size. Default `6000`. Configurable via experiment `chat.advanced.inlineEdits.xtabProvider.globalBudget.totalTokens`. | -| `globalBudget.order` | Ordered list of parts. Earlier parts get budget first; their surplus flows to later parts. | -| `globalBudget.shares` | `Record`. Each part's fraction of `totalTokens`. Must sum to `1 ± 1e-3` across `order`. | +| --- | --- | +| `globalBudget.totalTokens` | The single pool size. Default `7500`. Set via the `totalTokens` field of the experiment JSON string `chat.advanced.inlineEdits.xtabProvider.globalBudget`. | +| `globalBudget.order` | Ordered list of **rendered** parts. Earlier parts get budget first; their surplus flows to later parts. `currentFile` is not listed here. | +| `globalBudget.shares` | `Record` — one fraction of `totalTokens` per rendered part **and** for `currentFile`. Must sum to `1 ± 1e-3` across `order` plus `currentFile`. | ### Defaults `GlobalBudgetOptions.DEFAULT_ORDER`: -``` +```javascript ['languageContext', 'recentlyViewedDocuments', 'neighborFiles', 'diffHistory'] ``` `GlobalBudgetOptions.DEFAULT_SHARES` (volume-neutral with today's per-part caps): -| Part | Share | -|---|---| -| `recentlyViewedDocuments` | 2/6 | -| `languageContext` | 2/6 | -| `neighborFiles` | 1/6 | -| `diffHistory` | 1/6 | +| Part | Share | Base budget at `totalTokens = 7500` | +| --- | --- | --- | +| `currentFile` | 1500/7500 | 1500 | +| `recentlyViewedDocuments` | 2000/7500 | 2000 | +| `languageContext` | 2000/7500 | 2000 | +| `neighborFiles` | 1000/7500 | 1000 | +| `diffHistory` | 1000/7500 | 1000 | + +`GlobalBudgetOptions.DEFAULT_TOTAL_TOKENS` = `7500`. -`GlobalBudgetOptions.DEFAULT_TOTAL_TOKENS` = `6000`. +These shares reproduce today's per-part caps exactly: `currentFile.maxTokens` 1500, +`recentlyViewedDocuments` 2000, `languageContext` 2000, `neighborFiles` 1000, +`diffHistory` 1000. The pool total (`7500`) is the sum of those caps, so enabling +the default global budget neither grows nor shrinks any rendered part's base +allocation. The default order places `languageContext` first because it is often disabled or empty, donating its share to the always-on `recentlyViewedDocuments` next in -line. +line. Whatever the cascade does not use carries to `finalSurplus` and is handed +to the current file's clip. + +`GlobalBudgetOptions.currentFileBudget(gb)` returns \`floor(totalTokens \* +shares.currentFile)\` — the single source of truth for the current file's **base** +share. The current file's actual clip cap is this base plus the cascade's +`finalSurplus`. ## Algorithm -``` -surplus ← 0 +```javascript +surplus ← 0 // the current file never donates, so the cascade always seeds 0 for part in order: budget ← max(0, floor(surplus + totalTokens * shares[part])) consumed ← runSubBuilder(part, maxTokens: budget) // sub-builder ≤ budget surplus ← max(0, budget - consumed) // → next part only +// end-of-loop surplus is returned as finalSurplus and added to the current file's clip cap ``` Notes: +- The cascade is **always seeded with `0`**. The current file is clipped after the + cascade and only ever *receives* leftover, so it has nothing to donate forward. - The cascade calls the existing sub-builder for each part, overriding only its `maxTokens` (other options are inherited from `opts`): - - `recentlyViewedDocuments` → `buildCodeSnippetsUsingPagedClipping` - - `languageContext` → `appendLanguageContextSnippets` - - `neighborFiles` → `appendNeighborFileSnippets` - - `diffHistory` → `getEditDiffHistory` + - `recentlyViewedDocuments` → `buildCodeSnippetsUsingPagedClipping` + - `languageContext` → `appendLanguageContextSnippets` + - `neighborFiles` → `appendNeighborFileSnippets` + - `diffHistory` → `getEditDiffHistory` - Behavior of each sub-builder is unchanged. - Each sub-builder returns `tokensConsumed` using the **same internal accounting** it uses to make budget decisions (paged-clipping line cost for recently-viewed, raw-snippet cost for appenders, per-entry diff cost for diff history). Using that reported value to compute `surplus` keeps the cascade aligned with how each part actually charges against its budget. -- `surplus` is **forward-only**. Unused tokens at the *last* part are lost — the - cascade does not back-flow to earlier parts. +- `surplus` is **forward-only** between cascade parts. Unused tokens at the *last* + part are **not** lost: they form `finalSurplus`, which the current file's clip + reuses. +- The cascade returns its end-of-loop `surplus` as `CascadeResult.finalSurplus`. + `xtabProvider` grows the current file's clip budget by exactly this amount. See + [Clip the current file last](#clip-the-current-file-last). +- Defensive invariant: each sub-builder must report `0 ≤ tokensConsumed ≤ budget`. + The cascade `softAsserts` this per part (it does **not** silently clamp an + overspend down, which would hide the bug) so the conservation argument the + current-file clip relies on holds. ### Document tracking @@ -99,7 +137,7 @@ The cascade seeds `docsInPrompt` with the active document, and the `recentlyViewedDocuments` step adds the documents it includes. `neighborFiles` reads this set to avoid duplicating files already present, and then `appendNeighborFileSnippets` adds each included neighbor document to the set as well. This dependency is -why `validateGlobalBudget` rejects orderings where `neighborFiles` precedes +why `GlobalBudgetOptions.validate` rejects orderings where `neighborFiles` precedes `recentlyViewedDocuments`. The accumulated `docsInPrompt` is then passed to `getEditDiffHistory`, so any @@ -110,9 +148,10 @@ whose document is in `docsInPrompt`). ### Output The cascade's output mirrors the legacy `getRecentCodeSnippets` shape so the -rest of `getUserPrompt` is identical: +rest of `getUserPrompt` is identical, plus the `finalSurplus` used by the +current-file clip: -``` +```javascript { codeSnippets, // recentlyViewed + langCtx + neighbor joined by "\n\n" documents, // docsInPrompt @@ -120,14 +159,14 @@ rest of `getUserPrompt` is identical: editDiffHistory, nDiffsInPrompt, diffTokensInPrompt, + finalSurplus, // end-of-loop surplus, reused by the current-file clip } ``` ## Guarantees and limits -With `totalTokens = T` and shares `s_i` (assumed non-negative; negative shares -would be clamped to 0 by `max(0, floor(…))` in the budget computation, so the -floor guarantee below would not hold for them): +With `totalTokens = T` and shares `s_i` (validation guarantees they are finite and +non-negative — see [Validation](#validation)): - **Per-part floor**: part at index `i` always receives at least `floor(T * s_i)` tokens, regardless of what earlier parts do. @@ -135,111 +174,163 @@ floor guarantee below would not hold for them): floored sum `floor(…floor(floor(T * s_0) + T * s_1) + … + T * s_i)` — its own share plus everything donated by earlier parts, with `floor` applied at every step. Note this is generally smaller than `floor(T * (s_0 + … + s_i))`. +- **Current-file floor**: the current file always receives at least + `floor(T * shares.currentFile)` (its base share), and `finalSurplus ≥ 0` on top. - **Pool ceiling**: total cascade-managed tokens ≤ the ceiling of the last part - (always ≤ `T`, and typically strictly less due to per-step `floor` rounding). - `currentFile` and lint live outside this budget and add to the final prompt - size. -- **No back-flow**: surplus at the last part is wasted. + (always ≤ `T - floor(T * shares.currentFile)`). The current file then draws its + own base slice plus the cascade's `finalSurplus`, so the budgeted parts together + consume ≤ `T`; lint and scaffolding live outside the budget. +- **No back-flow between cascade parts**: surplus at the last cascade part is not + redistributed to earlier cascade parts — it flows to the current file as + `finalSurplus`. - **No intra-part fairness**: a single large item inside one part can consume that part's entire allocation; the cascade only addresses cross-part donation. ## Validation -`validateGlobalBudget` runs at the start of every cascade invocation and throws -on misconfiguration. The config is runtime-tunable (experiments), so failing -loudly is preferable to silent under/over-allocation. +`GlobalBudgetOptions.validate` runs at the start of every cascade invocation (and +again in `xtabProvider` before the current-file clip) and throws on +misconfiguration. The config is runtime-tunable (experiments), so failing loudly +is preferable to silent under/over-allocation. | Rule | Error | -|---|---| +| --- | --- | +| `totalTokens` is finite and `>= 0` | `globalBudget.totalTokens must be a finite, non-negative number, got X` | | `order` has no duplicate parts | `globalBudget.order contains duplicate part 'X'` | | Every part in `order` has a numeric `shares[part]` | `globalBudget.shares is missing entry for 'X'` | +| `shares.currentFile` is a number | `globalBudget.shares is missing entry for 'currentFile'` | +| Every share (order parts **and** `currentFile`) is finite and `>= 0` | `globalBudget.shares['X'] must be a finite, non-negative number, got Y` | | If both present, `recentlyViewedDocuments` precedes `neighborFiles` | `globalBudget.order must place 'recentlyViewedDocuments' before 'neighborFiles'` | -| Sum of `shares[part]` for parts in `order` ≈ 1 (epsilon `1e-3`) | `globalBudget.shares across order must sum to ~1, got ${sharesSum}` | +| Sum of `shares[part]` for parts in `order` plus `shares.currentFile` ≈ 1 (epsilon `1e-3`) | `globalBudget.shares across order must sum to ~1, got ${sharesSum}` | + +> **Why the non-negativity rule matters:** a negative share can still pass the +> "sum ≈ 1" check (e.g. one part `-0.25`, another `1.0`). At allocation time the +> negative part clamps to a `0` budget, but it still counted toward the sum, so the +> other parts over-allocate past the pool — which would let the current file's +> `finalSurplus` exceed the true leftover. Rejecting negative/non-finite shares up +> front keeps `Σ consumed ≤ totalTokens` provable. ## Wiring -The cascade is enabled via `ConfigKey.TeamInternal.InlineEditsXtabGlobalBudgetEnabled` -in `xtabProvider.ts` (~L1444): +The cascade is configured by a **single experiment-driven JSON string**, +`ConfigKey.TeamInternal.InlineEditsXtabGlobalBudget`, modelled after +`modelConfigurationString`. `xtabProvider.ts` (`getGlobalBudget()`) reads it and +parses it with `GlobalBudgetOptions.fromConfigString`: ```ts -globalBudget: globalBudgetEnabled - ? { - totalTokens: configService.getExperimentBasedConfig(InlineEditsXtabGlobalBudgetTotalTokens, expService), - order: GlobalBudgetOptions.DEFAULT_ORDER, - shares: GlobalBudgetOptions.DEFAULT_SHARES, +private getGlobalBudget(): GlobalBudgetOptions | undefined { + const configString = configService.getExperimentBasedConfig(InlineEditsXtabGlobalBudget, expService); + if (!configString) { + return undefined; // unset/empty → disabled, identical to prod } - : undefined, + const result = GlobalBudgetOptions.fromConfigString(configString); + if (result.isError()) { + telemetryService.sendMSFTTelemetryEvent('incorrectNesGlobalBudgetConfig', { errorMessage: result.err, configValue: configString }); + return undefined; // bad config → disabled, never crashes + } + return result.val; +} ``` +The JSON value defines the budget knobs together — `totalTokens`, `order`, and +`shares` — and every field is optional. Omitted fields fall back to +`DEFAULT_TOTAL_TOKENS` / `DEFAULT_ORDER` / `DEFAULT_SHARES`, so: + +- `undefined` / unset / `""` → global budget **disabled** (prod default, byte-identical legacy path). +- `{}` → **enabled** with the volume-neutral defaults. +- `{"totalTokens":6000}` → enabled, only the pool size overridden. +- `{"totalTokens":12000,"order":[…],"shares":{…}}` → fully custom. + +`fromConfigString` structurally validates the JSON (via `GlobalBudgetOptions.VALIDATOR`), +merges it over the defaults, then runs the semantic `GlobalBudgetOptions.validate` +(see [Validation](#validation)); any parse, structural, or semantic failure +returns a `Result.error` and disables the budget. When `shares` is provided it +must list **every** part (the rendered parts plus `currentFile`) — partial +`shares` objects are rejected so the pool stays fully allocated. + +When the budget is enabled, `xtabProvider` gathers the cascade inputs, runs the +cascade, and then clips the current file with cap +`currentFileBudget(globalBudget) + cascade.finalSurplus` (instead of the +standalone `currentFile.maxTokens`). The already-run cascade is threaded into +`getUserPrompt` as `precomputedCascade` so it renders exactly once. See +[Clip the current file last](#clip-the-current-file-last). + Experiment-controlled settings: | Setting | Default | Purpose | -|---|---|---| -| `chat.advanced.inlineEdits.xtabProvider.globalBudget.enabled` | `false` | Master switch | -| `chat.advanced.inlineEdits.xtabProvider.globalBudget.totalTokens` | `6000` | Pool size | +| --- | --- | --- | +| `chat.advanced.inlineEdits.xtabProvider.globalBudget` | `undefined` | JSON string defining `totalTokens`, `order`, and `shares`; unset/empty disables the budget | + +> **Migration note:** the old `globalBudget.enabled` (boolean) and +> `globalBudget.totalTokens` (number) settings have been **replaced** by this +> single JSON string. Any live experiment treatment that pinned those keys must +> migrate: `enabled:true` + `totalTokens:N` becomes the JSON `{"totalTokens":N}`, +> and a bare `enabled:true` becomes `{}`. Because `totalTokens` also funds +> `currentFile`, treatments that pinned an old total (e.g. `6000` or `8000`) +> should move to `{}` (or `7500`) to stay volume-neutral. ## Worked examples -All examples use `DEFAULT_ORDER`, `DEFAULT_SHARES`, and `totalTokens = 5000`. +All examples use `DEFAULT_ORDER`, `DEFAULT_SHARES`, and `totalTokens = 7500`. -Base allocations: `floor(5000 * share)` per part → +Base allocations: `floor(7500 * share)` per part → | Part | Base allocation | -|---|---| -| `languageContext` | 1666 | -| `recentlyViewedDocuments` | 1666 | -| `neighborFiles` | 833 | -| `diffHistory` | 833 | - -Sum = 4998 (2 tokens lost to per-step `floor`). - -### Example A — `languageContext` disabled; recently-viewed wants a lot - -| Part | surplus in | budget | consumed | surplus out | -|---|---|---|---|---| -| `languageContext` | 0 | 1666 | 0 | 1666 | -| `recentlyViewedDocuments` | 1666 | 3332 | 3332 | 0 | -| `neighborFiles` | 0 | 833 | 500 | 333 | -| `diffHistory` | 333 | 1166 | 1166 | 0 | +| --- | --- | +| `languageContext` | 2000 | +| `recentlyViewedDocuments` | 2000 | +| `neighborFiles` | 1000 | +| `diffHistory` | 1000 | +| `currentFile` (clipped last) | 1500 | -Tokens placed: 0 + 3332 + 500 + 1166 = **4998**. The disabled language-context -share doubled what recently-viewed got. +Sum = 7500. The cascade iterates only the four rendered parts, seeded with `0`. +Its end-of-loop surplus (`finalSurplus`) is added to the current file's base +allocation, shown as the final row's `budget`. -### Example B — modest language context; large single recently-viewed file; no neighbors +### Example A — cascade parts modest; current file absorbs the leftover | Part | surplus in | budget | consumed | surplus out | -|---|---|---|---|---| -| `languageContext` | 0 | 1666 | 800 | 866 | -| `recentlyViewedDocuments` | 866 | 2532 | 2532 | 0 | -| `neighborFiles` | 0 | 833 | 0 | 833 | -| `diffHistory` | 833 | 1666 | 1500 | 166 (wasted) | +| --- | --- | --- | --- | --- | +| `languageContext` | 0 | 2000 | 0 | 2000 | +| `recentlyViewedDocuments` | 2000 | 4000 | 1500 | 2500 | +| `neighborFiles` | 2500 | 3500 | 500 | 3000 | +| `diffHistory` | 3000 | 4000 | 500 | 3500 | +| `currentFile` (clipped last) | 3500 (`finalSurplus`) | 1500 + 3500 = 5000 | 5000 | 0 | -Tokens placed: 800 + 2532 + 0 + 1500 = **4832**. The trailing 166 from the last -part is lost (no back-flow). +Tokens placed: 0 + 1500 + 500 + 500 + 5000 = **7500**. The cascade consumed only +2500, so its 3500 leftover flowed into the current file, which grew from its 1500 +base to 5000 and trimmed less. -### Example C — everything modest +### Example B — empty cascade; current file reuses the whole pool | Part | surplus in | budget | consumed | surplus out | -|---|---|---|---|---| -| `languageContext` | 0 | 1666 | 400 | 1266 | -| `recentlyViewedDocuments` | 1266 | 2932 | 1500 | 1432 | -| `neighborFiles` | 1432 | 2265 | 900 | 1365 | -| `diffHistory` | 1365 | 2198 | 600 | 1598 (wasted) | +| --- | --- | --- | --- | --- | +| `languageContext` | 0 | 2000 | 0 | 2000 | +| `recentlyViewedDocuments` | 2000 | 4000 | 0 | 4000 | +| `neighborFiles` | 4000 | 5000 | 0 | 5000 | +| `diffHistory` | 5000 | 6000 | 0 | 6000 | +| `currentFile` (clipped last) | 6000 (`finalSurplus`) | 1500 + 6000 = 7500 | ≤ 7500 | — | -Tokens placed: 400 + 1500 + 900 + 600 = **3400**. Each later part received a -generous inflated cap but had no material to fill it. +With no language context, empty history, and neighbors disabled, every cascade +part consumes `0`, so the entire non-currentFile pool (`2000 + 2000 + 1000 + 1000 += 6000`) carries to `finalSurplus`. The current file's clip cap becomes `1500 + +6000 = 7500 = T` — it effectively reuses the whole pool. This is the common case +for a file edited in isolation. -### Example D — large `languageContext`, no donation +### Example C — cascade fills its pool; current file gets only its base | Part | surplus in | budget | consumed | surplus out | -|---|---|---|---|---| -| `languageContext` | 0 | 1666 | 1666 | 0 | -| `recentlyViewedDocuments` | 0 | 1666 | 1500 | 166 | -| `neighborFiles` | 166 | 999 | 900 | 99 | -| `diffHistory` | 99 | 932 | 800 | 132 (wasted) | - -Tokens placed: 1666 + 1500 + 900 + 800 = **4866**. Each part stayed at or below -its floor; nothing was starved. +| --- | --- | --- | --- | --- | +| `languageContext` | 0 | 2000 | 2000 | 0 | +| `recentlyViewedDocuments` | 0 | 2000 | 2000 | 0 | +| `neighborFiles` | 0 | 1000 | 1000 | 0 | +| `diffHistory` | 0 | 1000 | 1000 | 0 | +| `currentFile` (clipped last) | 0 (`finalSurplus`) | 1500 + 0 = 1500 | 1500 | 0 | + +Tokens placed: 2000 + 2000 + 1000 + 1000 + 1500 = **7500**. Every cascade part +filled its own share exactly, so `finalSurplus = 0` and the current file falls +back to its 1500 base — the per-part floor. The current file never shrinks below +this base. ## Disabled parts @@ -248,51 +339,53 @@ Parts can be disabled by configuration (`languageContext.enabled = false`, context response, empty neighbor snippets, no edit history). Those parts remain in `order` — the wired-in code always uses `DEFAULT_ORDER`/`DEFAULT_SHARES` — so their slot still runs, just with `consumed = 0`, and their full share donates -forward. +forward. Whatever reaches the end of the cascade becomes `finalSurplus` and is +reused by the current file's clip rather than wasted. ### Effective caps when both `languageContext` and `neighborFiles` are off With `DEFAULT_ORDER` and pool `T`, applying the per-step floor at every donation -step (let `C_rv = floor(floor(T·2/6) + T·2/6)` be the effective cap on -recently-viewed): +step (the cascade is seeded `0`, so let \`C\_rv = floor(floor(T·langCtxShare) + T·rvShare)\` be +the effective cap on recently-viewed): | Part | Effective cap | -|---|---| +| --- | --- | | `languageContext` | 0 (consumed) | -| `recentlyViewedDocuments` | `C_rv` (own 2/6 + langCtx's 2/6, with per-step `floor`) | +| `recentlyViewedDocuments` | `C_rv` (langCtx's share + own share, with per-step `floor`) | | `neighborFiles` | 0 (consumed) | -| `diffHistory` | `floor(floor((C_rv − consumed_rv) + T·1/6) + T·1/6)` (own 1/6 + neighbors' 1/6 + recently-viewed's leftover, with per-step `floor`) | +| `diffHistory` | `floor(floor((C_rv − consumed_rv) + T·neighborShare) + T·diffShare)` (own share + neighbors' share + recently-viewed's leftover, with per-step `floor`) | -At `T = 5000` (so `C_rv = floor(1666 + 1666.666…) = 3332` and the total -cascade pool ceiling is `floor(floor(3332 + 833.333…) + 833.333…) = 4998`, -not `5000`, because of per-step flooring): +At `T = 7500`, \`C\_rv = floor(2000 + 2000) = 4000\` and the total cascade pool +ceiling is \`floor(floor(4000 + 1000) + 1000) = 6000\`. Whatever `diffHistory` +leaves becomes `finalSurplus` for the current file: -| `recentlyViewedDocuments` consumed | `diffHistory` cap | -|---|---| -| 3332 (fills cap) | 1666 | -| 1500 | 3498 | -| 0 | 4998 (whole cascade pool) | +| `recentlyViewedDocuments` consumed | `diffHistory` cap | `finalSurplus` → current file | +| --- | --- | --- | +| 4000 (fills cap) | 2000 | 0 (current file at base 1500) | +| 1500 | 4500 | up to 4500 | +| 0 | 6000 (whole cascade pool) | up to 6000 (current file up to 7500) | -Worked example with both enabled parts hungry at `T = 5000`: +Worked example with both enabled parts hungry at `T = 7500`: | Part | surplus in | budget | consumed | surplus out | -|---|---|---|---|---| -| `languageContext` | 0 | 1666 | 0 | 1666 | -| `recentlyViewedDocuments` | 1666 | 3332 | 3332 | 0 | -| `neighborFiles` | 0 | 833 | 0 | 833 | -| `diffHistory` | 833 | 1666 | 1666 | 0 | +| --- | --- | --- | --- | --- | +| `languageContext` | 0 | 2000 | 0 | 2000 | +| `recentlyViewedDocuments` | 2000 | 4000 | 4000 | 0 | +| `neighborFiles` | 0 | 1000 | 0 | 1000 | +| `diffHistory` | 1000 | 2000 | 2000 | 0 | +| `currentFile` (clipped last) | 0 (`finalSurplus`) | 1500 | 1500 | 0 | -Total placed: **4998**. Recently-viewed absorbs langCtx's donation; diff history -absorbs neighbors' donation. Nothing is permanently wasted unless the *last* -part (`diffHistory`) also under-fills its budget — diff's surplus has no next -part to flow to. +Cascade placed **6000** (recently-viewed absorbed langCtx's donation; diff history +absorbed neighbors' donation), leaving `finalSurplus = 0`, so the current file +stays at its 1500 base for a total of 7500. ### Ordering caveat -`recentlyViewedDocuments` has **first claim** on the donation pool from -`languageContext` because it sits earlier in `order`. If recently-viewed is -hungry, diff history only sees `neighbors' 1/6 + own 1/6 = 2/6` (1666 at -`T = 5000`) — it cannot reach `languageContext`'s share directly. +`recentlyViewedDocuments` has **first claim** on the cascade donation pool +(`languageContext`'s share) because it sits earlier in `order`. If recently-viewed +is hungry, diff history only sees \`neighbors' + own share = 2000 at T = 7500\` +— it cannot reach `languageContext`'s share directly. Whatever +diff history (the last cascade part) leaves still flows to the current file. To make diff history share the donation, you would need to either reorder so `diffHistory` precedes `recentlyViewedDocuments`, or rebalance `shares`. The @@ -305,14 +398,98 @@ rebalance `shares` to sum to 1 — the donation would then be baked in staticall rather than emerging from the cascade. The wired-in code does not do this; it always passes `DEFAULT_ORDER` and `DEFAULT_SHARES`. -## Composition with `currentFile` +## Clip the current file last + +The current file is the natural sink for the cascade's leftover because it is the +one part clipped *around* a point of interest (the cursor), so it can always +absorb more context. Rather than build the prompt, measure what is unused, then +rebuild the current file bigger (a two-pass approach that risks double-counting +the leftover), the implementation simply **clips the current file last**: run the +cascade first, then size the current file with all the budget the cascade did not +use. + +### Mechanism + +In `xtabProvider`, under a global budget: + +1. Gather the cascade inputs (language context, neighbor snippets) — these do + **not** depend on the current-file clip, so they can be produced first. +2. Run `runGlobalBudgetCascade(...)` (seeded with `0`; the current file donates + nothing, so the cascade only ever *gives*). +3. Clip the current file **last** with cap + `currentFileBudget + cascade.finalSurplus`. +4. Assemble the prompt, passing the already-computed cascade through to + `getUserPrompt` as `precomputedCascade`. `getUserPrompt` honors + `precomputedCascade` only when `globalBudget` is set, so the cascade runs + exactly **once** and the rendered snippets match the sizing. + +When `globalBudget` is `undefined` (prod default) none of this applies: the +current file is clipped to its own `currentFile.maxTokens`, the inputs are +gathered after, and the cascade is not run at all — byte-identical to the legacy +path. + +### Conservation (proved) -`currentFile` is sized **outside** the cascade by -`createTaggedCurrentFileContentUsingPagedClipping`, which clips around the -cursor / edit window up to `currentFile.maxTokens`. The clipped string is -passed into `getUserPrompt` via `taggedCurrentDocLines` and concatenated -between the cascade-managed `recent_files` and `edit_history` blocks. The -cascade never sees nor influences current-file sizing. +Because the current file does not donate, budget flows in a single direction +(cascade → current file), so there is no double-counting. With the cascade seeded +at `0`, the end-of-loop surplus telescopes to + +``` +finalSurplus ≤ Σ(totalTokens · shareᵢ) for i in order + − Σ(consumedᵢ) = (T − T·share_cf) − C_cascade +``` + +so the current file's clip cap is + +``` +cfBudget = floor(T · share_cf) + finalSurplus ≤ T · (Σ all shares) − C_cascade +⇒ C_cf + C_cascade ≤ T · (Σ all shares) (total bounded by the pool) ✅ +``` -Final prompt size ≈ `currentFile.maxTokens` + ≤ `totalTokens` (cascade) + -lint + tags/scaffolding + postscript. +and since `finalSurplus ≥ 0`, `cfBudget ≥ floor(T · share_cf)` — the current file +**never shrinks below** its base share. Non-negative, validated shares (see +[Validation](#validation)) are what make the first inequality hold. + +> **Caveat — share-sum tolerance.** `validate` accepts `|Σ shares − 1| ≤ 1e-3`, so +> the bound above is `T · (Σ shares)`, not exactly `T`. A config whose shares sum +> slightly above 1 can over-allocate by at most `~1e-3 · T` (≈ 7.5 tokens at the +> default `T = 7500`). Shares summing to exactly 1 (the defaults do) give the clean +> `≤ T` bound. Under-allocation (sum < 1) simply wastes a little budget. + +> **Caveat — internal accounting, not the full rendered prompt.** "≤ `T`" is over +> the budgeted parts' *internal* token accounting (paged-clipping line cost, +> raw-snippet cost, diff-entry cost). The fully rendered prompt also carries tag +> wrappers, related-info scaffolding, lint, and the postscript, which live outside +> the pool. So the guarantee is "the budgeted parts together consume ≤ `T`", not +> "the whole prompt is ≤ `T` characters". Tests assert current-file-region +> **growth** and internal accounting, not an absolute full-prompt bound. + +### Worked example + +`T = 6000`, `languageContext` disabled, cascade consumes `rv 1500 + neighbor 500 ++ diff 500 = 2500` (`C_cascade = 2500`), `share_cf = 1500/7500 = 1/5` ⇒ base +`currentFileBudget = floor(6000 · 1/5) = 1200`: + +- The cascade is seeded `0`; its `finalSurplus = (1600 + 1600 + 800 + 800) − 2500 + = 2300`. +- The current file is clipped last to `1200 + 2300 = 3500` (= `6000 − 2500`). + +The current file grows `1200 → 3500`, absorbing exactly the 2300 the cascade left +unused — matching the intuition "budget 6k, first build consumed 4k ⇒ give the +current file the remaining 2k". + +### Trade-offs and caveats + +- **Current file does not donate.** Budget only ever flows cascade → current file. + When the current file is small, its base share is **not** handed to the cascade + parts (they get only their own shares). A deployment optimizing for rich + *neighbor/history* context (rather than current-file context) would need to + rebalance `shares` to give those parts more. +- **Reordered awaits.** Because the cascade runs before the current-file clip, + language-context and neighbor-snippet gathering happen before the current file is + clipped. Any `PromptTooLarge('currentFile')` early-return / cancellation reason + therefore fires *after* those awaits under a global budget. This is an acceptable + consequence of the opt-in feature; the prod path keeps the legacy ordering. +- **Next-cursor predictor unaffected.** `xtabNextCursorPredictor` keeps its own + dedicated current-file cap and never carries a global budget into its prompt, so + it is byte-identical regardless of this feature. diff --git a/extensions/copilot/src/extension/xtab/common/promptCrafting.ts b/extensions/copilot/src/extension/xtab/common/promptCrafting.ts index 869fbe8f3a5ad..aaa17174a0f0c 100644 --- a/extensions/copilot/src/extension/xtab/common/promptCrafting.ts +++ b/extensions/copilot/src/extension/xtab/common/promptCrafting.ts @@ -12,7 +12,7 @@ import { IXtabHistoryEntry } from '../../../platform/inlineEdits/common/workspac import { ContextKind, TraitContext } from '../../../platform/languageServer/common/languageContextService'; import { Result } from '../../../util/common/result'; import { range } from '../../../util/vs/base/common/arrays'; -import { assertNever } from '../../../util/vs/base/common/assert'; +import { assertNever, softAssert } from '../../../util/vs/base/common/assert'; import { StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit'; import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange'; import { getEditDiffHistory } from './diffHistoryForPrompt'; @@ -38,6 +38,15 @@ export class PromptPieces { public readonly computeTokens: (s: string) => number, public readonly opts: PromptOptions, public readonly neighborSnippets?: readonly INeighborFileSnippet[], + /** + * A cascade result computed by the caller (the provider, which runs the + * cascade first so it can clip `currentFile` last to `currentFileBudget + + * finalSurplus`). When provided, {@link getUserPrompt} renders these + * snippets instead of running the cascade itself, guaranteeing the surplus + * used to size the current file matches the snippets that end up in the + * prompt. Only honored when `opts.globalBudget` is set. + */ + public readonly precomputedCascade?: CascadeResult, ) { } } @@ -51,7 +60,7 @@ export interface UserPromptResult { export function getUserPrompt(promptPieces: PromptPieces): UserPromptResult { - const { activeDoc, xtabHistory, taggedCurrentDocLines, areaAroundCodeToEdit, langCtx, aggressivenessLevel, lintErrors, computeTokens, opts, neighborSnippets } = promptPieces; + const { activeDoc, xtabHistory, taggedCurrentDocLines, areaAroundCodeToEdit, langCtx, aggressivenessLevel, lintErrors, computeTokens, opts, neighborSnippets, precomputedCascade } = promptPieces; const currentFileContent = taggedCurrentDocLines.join('\n'); let recentlyViewedCodeSnippets: string; @@ -62,7 +71,10 @@ export function getUserPrompt(promptPieces: PromptPieces): UserPromptResult { let diffTokensInPrompt: number; if (opts.globalBudget !== undefined) { - const cascade = runGlobalBudgetCascade(activeDoc, xtabHistory, langCtx, computeTokens, opts, neighborSnippets, opts.globalBudget); + // Reuse a cascade the caller already ran (the provider runs it first so it can + // clip the current file last from `finalSurplus`), or run it now for callers + // that set a global budget without precomputing (e.g. tests). + const cascade = precomputedCascade ?? runGlobalBudgetCascade(activeDoc, xtabHistory, langCtx, computeTokens, opts, neighborSnippets, opts.globalBudget); recentlyViewedCodeSnippets = cascade.codeSnippets; docsInPrompt = cascade.documents; neighborSnippetsResult = cascade.neighborSnippetsResult; @@ -159,13 +171,19 @@ ${PromptTags.EDIT_HISTORY.end}`; return { prompt: trimmedPrompt, nDiffsInPrompt, diffTokensInPrompt, neighborSnippetsResult }; } -interface CascadeResult { +export interface CascadeResult { readonly codeSnippets: string; readonly documents: Set; readonly neighborSnippetsResult: AppendNeighborFileSnippetsResult | undefined; readonly editDiffHistory: string; readonly nDiffsInPrompt: number; readonly diffTokensInPrompt: number; + /** + * Budget left unused after the last part in `order` ran. The provider adds it + * to the current file's clip budget (`currentFileBudget + finalSurplus`) so the + * current file, which is clipped last, reuses whatever the cascade left unused. + */ + readonly finalSurplus: number; } /** @@ -174,17 +192,21 @@ interface CascadeResult { * `surplus + totalTokens * shares[part]` and run that sub-builder with the override. * Unspent budget cascades to the next part. * - * Sub-builders are invoked using existing helpers so behavior of each individual - * part is unchanged. `currentFile` and `lintOptions` are intentionally excluded - * and continue using their own per-part caps. + * The cascade starts with surplus `0` and renders only the parts in `order`. + * `currentFile` is not rendered here (it is clipped separately by the caller) and + * `lintOptions` is excluded entirely; both keep their own per-part caps for + * clipping. Whatever budget is unused after the last part is returned as + * {@link CascadeResult.finalSurplus}; the provider adds it to the current file's + * clip budget so the current file reuses the leftover (it is clipped last). * - * Each sub-builder reports `tokensConsumed` using the same internal accounting - * it uses to make budget decisions (paged-clipping line cost, raw-snippet cost - * for appenders, diff-entry cost for history). The cascade uses that reported - * value to compute `surplus`, which keeps the cascade aligned with how each - * part actually charges against its budget. + * Sub-builders are invoked using existing helpers so behavior of each individual + * part is unchanged. Each sub-builder reports `tokensConsumed` using the same + * internal accounting it uses to make budget decisions (paged-clipping line cost, + * raw-snippet cost for appenders, diff-entry cost for history). The cascade uses + * that reported value to compute `surplus`, which keeps the cascade aligned with + * how each part actually charges against its budget. */ -function runGlobalBudgetCascade( +export function runGlobalBudgetCascade( activeDoc: StatelessNextEditDocument, xtabHistory: readonly IXtabHistoryEntry[], langCtx: LanguageContextResponse | undefined, @@ -193,7 +215,7 @@ function runGlobalBudgetCascade( neighborSnippets: readonly INeighborFileSnippet[] | undefined, globalBudget: GlobalBudgetOptions, ): CascadeResult { - validateGlobalBudget(globalBudget); + GlobalBudgetOptions.validate(globalBudget); const recentlyViewedSnippets: string[] = []; const langCtxSnippets: string[] = []; @@ -250,6 +272,11 @@ function runGlobalBudgetCascade( default: assertNever(part); } + // The conservation guarantee (current file + cascade <= totalTokens) relies + // on every sub-builder reporting `0 <= tokensConsumed <= budget`. Surface a + // violation (never silently clamp `tokensConsumed` down — that would hide + // real overspend and let the prompt exceed the pool). + softAssert(tokensConsumed >= 0 && tokensConsumed <= budget, `globalBudget part '${part}' reported tokensConsumed=${tokensConsumed} outside [0, ${budget}]`); surplus = Math.max(0, budget - tokensConsumed); } @@ -262,44 +289,10 @@ function runGlobalBudgetCascade( editDiffHistory, nDiffsInPrompt, diffTokensInPrompt, + finalSurplus: surplus, }; } -/** - * Validate {@link GlobalBudgetOptions} since it is runtime-configurable - * (e.g. via experiments). Catches misconfigurations that would otherwise - * cause silent, hard-to-debug behavior: - * - duplicate parts in `order` (would render the same part twice) - * - missing share for any part in `order` - * - shares not summing to ~1 across `order` (would over/under-allocate) - * - `neighborFiles` ordered before `recentlyViewedDocuments` (the former - * consults `docsInPrompt` populated by the latter) - */ -function validateGlobalBudget(globalBudget: GlobalBudgetOptions): void { - const seen = new Set(); - for (const part of globalBudget.order) { - if (seen.has(part)) { - throw new Error(`globalBudget.order contains duplicate part '${part}'`); - } - seen.add(part); - if (typeof globalBudget.shares[part] !== 'number') { - throw new Error(`globalBudget.shares is missing entry for '${part}'`); - } - } - - const recentIdx = globalBudget.order.indexOf('recentlyViewedDocuments'); - const neighborIdx = globalBudget.order.indexOf('neighborFiles'); - if (recentIdx !== -1 && neighborIdx !== -1 && neighborIdx < recentIdx) { - throw new Error(`globalBudget.order must place 'recentlyViewedDocuments' before 'neighborFiles'`); - } - - const sharesSum = globalBudget.order.reduce((sum, part) => sum + globalBudget.shares[part], 0); - const epsilon = 1e-3; - if (Math.abs(sharesSum - 1) > epsilon) { - throw new Error(`globalBudget.shares across order must sum to ~1, got ${sharesSum}`); - } -} - function wrapInBackticks(content: string) { return `\`\`\`\n${content}\n\`\`\``; } diff --git a/extensions/copilot/src/extension/xtab/node/xtabPatchResponseHandler.ts b/extensions/copilot/src/extension/xtab/node/xtabPatchResponseHandler.ts index 804ad0809266e..542a6a109f287 100644 --- a/extensions/copilot/src/extension/xtab/node/xtabPatchResponseHandler.ts +++ b/extensions/copilot/src/extension/xtab/node/xtabPatchResponseHandler.ts @@ -13,6 +13,7 @@ import { equals as arraysEqual } from '../../../util/vs/base/common/arrays'; import { isAbsolute } from '../../../util/vs/base/common/path'; import { URI } from '../../../util/vs/base/common/uri'; import { LineReplacement } from '../../../util/vs/editor/common/core/edits/lineEdit'; +import { DefaultLinesDiffComputer } from '../../../util/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; import { LineRange } from '../../../util/vs/editor/common/core/ranges/lineRange'; import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange'; import { AbstractText } from '../../../util/vs/editor/common/core/text/abstractText'; @@ -31,23 +32,29 @@ class Patch { */ public readonly filePath: string, public readonly lineNumZeroBased: number, + /** + * Zero-based index of the model-emitted patch this object represents. Patches + * derived from the same model header (e.g. the early + continuation patches of a + * progressive ghost-text reveal) share the same `patchIndex`. + */ + public readonly patchIndex: number, ) { } - public static ofLine(line: string): Patch | null { + public static ofLine(line: string, patchIndex: number): Patch | null { const match = line.match(/^(.+):(\d+)$/); if (!match) { return null; } const [, filename, lineNumber] = match; - return new Patch(filename, parseInt(lineNumber, 10)); + return new Patch(filename, parseInt(lineNumber, 10), patchIndex); } /** * Creates a pure-insertion patch (no removed lines) at the given line. * Used for the continuation portion of a ghost-text progressive reveal. */ - public static insertion(filePath: string, lineNumZeroBased: number): Patch { - return new Patch(filePath, lineNumZeroBased); + public static insertion(filePath: string, lineNumZeroBased: number, patchIndex: number): Patch { + return new Patch(filePath, lineNumZeroBased, patchIndex); } addLine(line: string): boolean { @@ -281,6 +288,64 @@ function applyDuplicatePolicy( export namespace XtabPatchResponseHandler { + /** + * Upper bound on how long the per-patch diff may run before we give up and + * fall back to the original (unsplit) replacement. The inputs are a single + * patch's removed/added lines, so this is only a safety valve against + * pathological inputs. + */ + const SPLIT_DIFF_MAX_COMPUTATION_TIME_MS = 100; + + /** + * Splits a coarse patch replacement into the minimal set of sub-replacements + * by running a line diff between the removed and added lines. + * + * The diff-patch model emits a patch as a contiguous block of `-`/`+` lines, + * which `resolveEdit` turns into a single `LineReplacement` spanning every + * removed line. When only a subset of those lines actually changed (the model + * re-emitted surrounding context), a line-level diff recovers the minimal + * hunks, yielding several small replacements with the untouched lines left as + * context — a nicer suggestion shape. + * + * @param replacement The resolved (possibly dedup-trimmed) replacement; its + * `newLines` are the added lines and its `lineRange` anchors the result in + * the document. + * @param removedLines The original line content removed by the patch. Its + * length is expected to match `replacement.lineRange.length`. + * + * Returns the original replacement unchanged when there is nothing to gain: + * a pure insertion or deletion (one side empty), a diff that times out, or a + * diff that collapses to a single hunk. + */ + export function splitReplacement(replacement: LineReplacement, removedLines: readonly string[]): LineReplacement[] { + const addedLines = replacement.newLines; + if (removedLines.length === 0 || addedLines.length === 0) { + return [replacement]; + } + + const diff = new DefaultLinesDiffComputer().computeDiff([...removedLines], [...addedLines], { + ignoreTrimWhitespace: false, + maxComputationTimeMs: SPLIT_DIFF_MAX_COMPUTATION_TIME_MS, + computeMoves: false, + }); + if (diff.hitTimeout || diff.changes.length <= 1) { + return [replacement]; + } + + // `change.original`/`change.modified` are 1-based line ranges over + // `removedLines`/`addedLines`. Anchor the original side at the + // replacement's first removed line (`removedLines[0]` lives on + // `lineRange.startLineNumber`) and slice the new content from `addedLines`. + const baseLine = replacement.lineRange.startLineNumber; + return diff.changes.map(change => new LineReplacement( + new LineRange( + baseLine + change.original.startLineNumber - 1, + baseLine + change.original.endLineNumberExclusive - 1, + ), + addedLines.slice(change.modified.startLineNumber - 1, change.modified.endLineNumberExclusive - 1), + )); + } + export async function* handleResponse( linesStream: AsyncIterable, currentDocument: CurrentDocument, @@ -290,6 +355,7 @@ export namespace XtabPatchResponseHandler { parentTracer: ILogger, duplicateAdditionsMode: DuplicateAdditionsMode = DuplicateAdditionsMode.Off, enableProgressiveGhostText: boolean = false, + splitPatchOnDiff: boolean = false, ): AsyncGenerator { const tracer = parentTracer.createSubLogger(['XtabCustomDiffPatchResponseHandler', 'handleResponse']); const activeDocRelativePath = toUniquePath(activeDocumentId, workspaceRoot?.path); @@ -330,12 +396,18 @@ export namespace XtabPatchResponseHandler { } } - yield { - edit: lineReplacement, - isFromCursorJump: false, - targetDocument, - window, - } satisfies StreamedEdit; + const replacements = splitPatchOnDiff + ? splitReplacement(lineReplacement, edit.removedLines) + : [lineReplacement]; + for (const replacement of replacements) { + yield { + edit: replacement, + isFromCursorJump: false, + targetDocument, + window, + patchIndex: edit.patchIndex, + } satisfies StreamedEdit; + } } } catch (e: unknown) { if (e instanceof FetchStreamError) { @@ -387,14 +459,29 @@ export namespace XtabPatchResponseHandler { export async function* extractEdits(linesStream: AsyncIterable, cursorLineZeroBased?: number, activeDocRelativePath?: string): AsyncGenerator { let currentPatch: Patch | null = null; let isFirstPatch = true; + // Tracks whether we've already attempted progressive reveal (succeeds or fails only once). let progressiveRevealDone = false; + + // Monotonic 0-based index assigned to each real model patch header. Derived + // patches (ghost-text early + continuation) inherit their source's index. + let nextPatchIndex = 0; + // Parses a header line into a patch, allocating it the next patch index. + // Returns null for lines that aren't valid headers, which don't consume an index. + const parseNextPatchHeader = (line: string): Patch | null => { + const patch = Patch.ofLine(line, nextPatchIndex); + if (patch !== null) { + nextPatchIndex++; + } + return patch; + }; + for await (const line of linesStream) { if (line.trim() === ResponseTags.NO_EDIT) { break; } if (currentPatch === null) { - currentPatch = Patch.ofLine(line); + currentPatch = parseNextPatchHeader(line); continue; } if (currentPatch.addLine(line)) { @@ -407,13 +494,16 @@ export namespace XtabPatchResponseHandler { && currentPatch.removedLines.length >= 1 ) { if (isGhostTextPatch(currentPatch, cursorLineZeroBased, activeDocRelativePath)) { + // Both the early and continuation patches stem from the same + // model patch, so they share its index. + const sourcePatchIndex = currentPatch.patchIndex; // Yield the cursor-line replacement immediately - const earlyPatch = Patch.insertion(currentPatch.filePath, currentPatch.lineNumZeroBased); + const earlyPatch = Patch.insertion(currentPatch.filePath, currentPatch.lineNumZeroBased, sourcePatchIndex); earlyPatch.removedLines = [...currentPatch.removedLines]; earlyPatch.addedLines = [...currentPatch.addedLines]; yield earlyPatch; // Replace currentPatch with a continuation pure-insertion patch - currentPatch = Patch.insertion(currentPatch.filePath, currentPatch.lineNumZeroBased + 1); + currentPatch = Patch.insertion(currentPatch.filePath, currentPatch.lineNumZeroBased + 1, sourcePatchIndex); } progressiveRevealDone = true; } @@ -423,7 +513,7 @@ export namespace XtabPatchResponseHandler { if (currentPatch.removedLines.length > 0 || currentPatch.addedLines.length > 0) { yield currentPatch; } - currentPatch = Patch.ofLine(line); + currentPatch = parseNextPatchHeader(line); isFirstPatch = false; } if (currentPatch && (currentPatch.removedLines.length > 0 || currentPatch.addedLines.length > 0)) { diff --git a/extensions/copilot/src/extension/xtab/node/xtabProvider.ts b/extensions/copilot/src/extension/xtab/node/xtabProvider.ts index 4be3a966049fc..fa773eb2c0905 100644 --- a/extensions/copilot/src/extension/xtab/node/xtabProvider.ts +++ b/extensions/copilot/src/extension/xtab/node/xtabProvider.ts @@ -33,6 +33,7 @@ import { OptionalChatRequestParams, Prediction } from '../../../platform/network import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { ISimulationTestContext } from '../../../platform/simulationTestContext/common/simulationTestContext'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; +import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; import { raceFilter } from '../../../util/common/async'; import { AsyncIterUtils, AsyncIterUtilsExt } from '../../../util/common/asyncIterableUtils'; @@ -62,9 +63,9 @@ import { IgnoreImportChangesAspect } from '../../inlineEdits/node/importFilterin import { FetchStreamError } from '../common/fetchStreamError'; import { determineIsInlineSuggestionPosition } from '../common/inlineSuggestion'; import { LintErrors } from '../common/lintErrors'; -import { ClippedDocument, constructTaggedFile, getUserPrompt, N_LINES_ABOVE, N_LINES_AS_CONTEXT, N_LINES_BELOW, PromptPieces } from '../common/promptCrafting'; +import { ClippedDocument, constructTaggedFile, getUserPrompt, N_LINES_ABOVE, N_LINES_AS_CONTEXT, N_LINES_BELOW, PromptPieces, runGlobalBudgetCascade, CascadeResult } from '../common/promptCrafting'; import { countTokensForLines, toUniquePath } from '../common/promptCraftingUtils'; -import { ISimilarFilesContextService } from '../common/similarFilesContextService'; +import { INeighborFileSnippet, ISimilarFilesContextService } from '../common/similarFilesContextService'; import { nes41Miniv3SystemPrompt, simplifiedPrompt, systemPromptTemplate, unifiedModelSystemPrompt, xtab275SystemPrompt } from '../common/systemMessages'; import { PromptTags } from '../common/tags'; import { TerminalMonitor } from '../common/terminalOutput'; @@ -177,6 +178,7 @@ export class XtabProvider implements IStatelessNextEditProvider { @ILanguageDiagnosticsService private readonly langDiagService: ILanguageDiagnosticsService, @IIgnoreService private readonly ignoreService: IIgnoreService, @ISimilarFilesContextService private readonly similarFilesContextService: ISimilarFilesContextService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { this.userInteractionMonitor = this.instaService.createInstance(UserInteractionMonitor); this.terminalMonitor = this.instaService.createInstance(TerminalMonitor); @@ -250,6 +252,87 @@ export class XtabProvider implements IStatelessNextEditProvider { ); } + /** + * Gathers language context and neighbor snippets and clips the current file, returning the + * pieces {@link getUserPrompt} needs, or a {@link NoNextEditReason} when the request is + * cancelled mid-gathering or the current file cannot fit its budget. + * + * Under a global budget the current file is clipped LAST: the cascade runs first so the + * current file can reuse whatever budget it leaves unused (via `finalSurplus`), and the + * already-run cascade is returned as `precomputedCascade` so {@link getUserPrompt} renders + * it exactly once. With no global budget (prod default) the current file is clipped to its + * own per-part cap first, byte-identical to the legacy path. + */ + private async gatherContextAndClipCurrentFile( + globalBudget: xtabPromptOptions.GlobalBudgetOptions | undefined, + activeDocument: StatelessNextEditDocument, + request: StatelessNextEditRequest, + promptOptions: xtabPromptOptions.PromptOptions, + telemetry: StatelessNextEditTelemetryBuilder, + cancellationToken: CancellationToken, + gatherLanguageContext: () => Promise, + gatherNeighborSnippets: () => Promise, + clipCurrentFileToBudget: (overriddenMaxTokens: number | undefined) => Result<{ clippedTaggedCurrentDoc: ClippedDocument; areaAroundCodeToEdit: string }, 'outOfBudget'>, + ): Promise> { + if (globalBudget !== undefined) { + // Clip the current file LAST. Gather the cascade inputs (which do not depend + // on the current-file clip) and run the cascade first, then size the current + // file to `currentFileBudget + finalSurplus` so it reuses whatever budget the + // cascade left unused. The already-run cascade is threaded into + // `getUserPrompt` as `precomputedCascade` so it renders exactly once. + const langCtx = await gatherLanguageContext(); + if (cancellationToken.isCancellationRequested) { + return Result.error(new NoNextEditReason.GotCancelled('afterLanguageContextAwait')); + } + + const neighborSnippets = await gatherNeighborSnippets(); + if (cancellationToken.isCancellationRequested) { + return Result.error(new NoNextEditReason.GotCancelled('afterNeighborSnippetsAwait')); + } + + const cascade = runGlobalBudgetCascade(activeDocument, request.xtabEditHistory, langCtx, XtabProvider.computeTokens, promptOptions, neighborSnippets, globalBudget); + const currentFileBudget = xtabPromptOptions.GlobalBudgetOptions.currentFileBudget(globalBudget); + + const taggedCurrentFileContentResult = clipCurrentFileToBudget(currentFileBudget + cascade.finalSurplus); + if (taggedCurrentFileContentResult.isError()) { + return Result.error(new NoNextEditReason.PromptTooLarge('currentFile')); + } + const { clippedTaggedCurrentDoc, areaAroundCodeToEdit } = taggedCurrentFileContentResult.val; + telemetry.setNLinesOfCurrentFileInPrompt(clippedTaggedCurrentDoc.lines.length); + return Result.ok({ clippedTaggedCurrentDoc, areaAroundCodeToEdit, precomputedCascade: cascade, langCtx, neighborSnippets }); + } else { + // No global budget (prod default): clip the current file to its own per-part + // cap, then gather context. Byte-identical to the legacy path. + const taggedCurrentFileContentResult = clipCurrentFileToBudget(undefined); + if (taggedCurrentFileContentResult.isError()) { + return Result.error(new NoNextEditReason.PromptTooLarge('currentFile')); + } + const { clippedTaggedCurrentDoc, areaAroundCodeToEdit } = taggedCurrentFileContentResult.val; + // Record the clipped line count BEFORE the context-gathering awaits so it is + // still emitted when the request is cancelled mid-gathering (matches the + // legacy prod timing — the cancellation path below also reports telemetry). + telemetry.setNLinesOfCurrentFileInPrompt(clippedTaggedCurrentDoc.lines.length); + + const langCtx = await gatherLanguageContext(); + if (cancellationToken.isCancellationRequested) { + return Result.error(new NoNextEditReason.GotCancelled('afterLanguageContextAwait')); + } + + const neighborSnippets = await gatherNeighborSnippets(); + if (cancellationToken.isCancellationRequested) { + return Result.error(new NoNextEditReason.GotCancelled('afterNeighborSnippetsAwait')); + } + + return Result.ok({ clippedTaggedCurrentDoc, areaAroundCodeToEdit, precomputedCascade: undefined, langCtx, neighborSnippets }); + } + } + private async *doGetNextEditWithSelection( request: StatelessNextEditRequest, selection: Range | null, @@ -310,27 +393,37 @@ export class XtabProvider implements IStatelessNextEditProvider { const doesIncludeCursorTag = editWindowLines.some(line => line.includes(PromptTags.CURSOR)); const shouldRemoveCursorTagFromResponse = !doesIncludeCursorTag; // we'd like to remove the tag only if the original edit-window didn't include the tag - const taggedCurrentFileContentResult = constructTaggedFile( - currentDocument, - editWindowLinesRange, - areaAroundEditWindowLinesRange, - promptOptions, - XtabProvider.computeTokens, - { - includeLineNumbers: { - areaAroundCodeToEdit: xtabPromptOptions.IncludeLineNumbersOption.None, - currentFileContent: promptOptions.currentFile.includeLineNumbers, - } - } - ); - - if (taggedCurrentFileContentResult.isError()) { - return new NoNextEditReason.PromptTooLarge('currentFile'); + // Under a global budget the current file is clipped LAST so it reuses whatever + // budget the cascade parts (recently-viewed, language context, neighbors, diff + // history) leave unused: the cascade runs first, then the current file is + // clipped to `currentFileBudget + cascadeFinalSurplus`, so it trims less. With + // no global budget (prod default) the current file keeps its own per-part cap. + const globalBudget = promptOptions.globalBudget; + if (globalBudget !== undefined) { + xtabPromptOptions.GlobalBudgetOptions.validate(globalBudget); } - const { clippedTaggedCurrentDoc, areaAroundCodeToEdit } = taggedCurrentFileContentResult.val; - - telemetry.setNLinesOfCurrentFileInPrompt(clippedTaggedCurrentDoc.lines.length); + // Clips the current file to `overriddenMaxTokens` (or its per-part + // `currentFile.maxTokens` cap when `overriddenMaxTokens` is undefined), + // returning the tagged lines plus the area around the code to edit. + const clipCurrentFileToBudget = (overriddenMaxTokens: number | undefined) => { + const cfPromptOptions = overriddenMaxTokens !== undefined + ? { ...promptOptions, currentFile: { ...promptOptions.currentFile, maxTokens: overriddenMaxTokens } } + : promptOptions; + return constructTaggedFile( + currentDocument, + editWindowLinesRange, + areaAroundEditWindowLinesRange, + cfPromptOptions, + XtabProvider.computeTokens, + { + includeLineNumbers: { + areaAroundCodeToEdit: xtabPromptOptions.IncludeLineNumbersOption.None, + currentFileContent: promptOptions.currentFile.includeLineNumbers, + } + } + ); + }; const { aggressivenessLevel, userHappinessScore } = this.userInteractionMonitor.getAggressivenessLevel(); @@ -344,7 +437,7 @@ export class XtabProvider implements IStatelessNextEditProvider { telemetry.setXtabUserHappinessScore(userHappinessScore); } - const langCtx = await this.getAndProcessLanguageContext( + const gatherLanguageContext = () => this.getAndProcessLanguageContext( request, delaySession, activeDocument, @@ -354,23 +447,31 @@ export class XtabProvider implements IStatelessNextEditProvider { cancellationToken, ); - if (cancellationToken.isCancellationRequested) { - return new NoNextEditReason.GotCancelled('afterLanguageContextAwait'); - } - - const neighborSnippets = promptOptions.neighborFiles.enabled - ? await raceCancellation( + const gatherNeighborSnippets = () => promptOptions.neighborFiles.enabled + ? raceCancellation( raceTimeout( this.similarFilesContextService.getSnippetsForPrompt(activeDocument.id.uri, activeDocument.languageId, activeDocument.documentAfterEdits.value, currentDocument.cursorOffset), delaySession.getDebounceTime() ), cancellationToken, ) - : undefined; + : Promise.resolve(undefined); - if (cancellationToken.isCancellationRequested) { - return new NoNextEditReason.GotCancelled('afterNeighborSnippetsAwait'); + const contextResult = await this.gatherContextAndClipCurrentFile( + globalBudget, + activeDocument, + request, + promptOptions, + telemetry, + cancellationToken, + gatherLanguageContext, + gatherNeighborSnippets, + clipCurrentFileToBudget, + ); + if (contextResult.isError()) { + return contextResult.err; } + const { clippedTaggedCurrentDoc, areaAroundCodeToEdit, precomputedCascade, langCtx, neighborSnippets } = contextResult.val; const lintErrors = new LintErrors(activeDocument.id, currentDocument, this.langDiagService, request.xtabEditHistory); @@ -388,6 +489,7 @@ export class XtabProvider implements IStatelessNextEditProvider { XtabProvider.computeTokens, promptOptions, neighborSnippets, + precomputedCascade, ); const { prompt: userPrompt, nDiffsInPrompt, diffTokensInPrompt, neighborSnippetsResult } = getUserPrompt(promptPieces); @@ -944,6 +1046,7 @@ export class XtabProvider implements IStatelessNextEditProvider { const pseudoEditWindow = currentDocument.transformer.getOffsetRange(new Range(clippedTaggedCurrentDoc.keptRange.start + 1, 1, clippedTaggedCurrentDoc.keptRange.endExclusive, lastLineLength + 1)); const duplicateAdditionsMode = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabDuplicateAdditionsMode, this.expService); const fastYieldLineWithCursor = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabProviderPatchFastYieldLineWithCursor, this.expService); + const splitPatchOnDiff = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabSplitPatchOnDiff, this.expService); parseResult = new ResponseParseResult.DirectEdits( XtabPatchResponseHandler.handleResponse( linesStream, @@ -954,6 +1057,7 @@ export class XtabProvider implements IStatelessNextEditProvider { tracer, duplicateAdditionsMode, fastYieldLineWithCursor, + splitPatchOnDiff, ), ); break; @@ -1282,11 +1386,19 @@ export class XtabProvider implements IStatelessNextEditProvider { } const targetContent = new StringText(targetTextDoc.getText()); + const targetContentLines = targetContent.getLines(); + + if (prediction.lineNumber >= targetContentLines.length) { // >= because the line index is zero-based + tracer.trace(`Predicted cross-file cursor jump error: exceedsDocumentLines`); + telemetry.setNextCursorLineError('crossFile:exceedsDocumentLines'); + return new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, editWindow); + } + const syntheticDoc = new StatelessNextEditDocument( targetDocumentId, promptPieces.activeDoc.workspaceRoot, LanguageId.create(targetTextDoc.languageId), - targetContent.getLines(), + targetContentLines, LineEdit.empty, targetContent, new Edits(StringEdit, []), @@ -1446,13 +1558,7 @@ export class XtabProvider implements IStatelessNextEditProvider { }, lintOptions: undefined, includePostScript: true, - globalBudget: this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabGlobalBudgetEnabled, this.expService) - ? { - totalTokens: this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabGlobalBudgetTotalTokens, this.expService), - order: xtabPromptOptions.GlobalBudgetOptions.DEFAULT_ORDER, - shares: xtabPromptOptions.GlobalBudgetOptions.DEFAULT_SHARES, - } - : undefined, + globalBudget: this.getGlobalBudget(), }; const selectedModelConfig = this.modelService.selectedModelConfiguration(); @@ -1463,6 +1569,36 @@ export class XtabProvider implements IStatelessNextEditProvider { }; } + /** + * Resolve the opt-in global budget from its single experiment-driven JSON + * config string (mirrors `modelConfigurationString`). Returns `undefined` + * — disabling the global budget, identical to prod — when the string is + * unset, empty, or fails to parse/validate. Parse failures are reported via + * telemetry so misconfigured experiments are observable. + */ + private getGlobalBudget(): xtabPromptOptions.GlobalBudgetOptions | undefined { + const configString = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabGlobalBudget, this.expService); + if (!configString) { + return undefined; + } + + const result = xtabPromptOptions.GlobalBudgetOptions.fromConfigString(configString); + if (result.isError()) { + /* __GDPR__ + "incorrectNesGlobalBudgetConfig" : { + "owner": "ulugbekna", + "comment": "Capture if the experiment-driven NES global budget config string is invalid or malformed, so the global budget was disabled.", + "errorMessage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error message from parsing or validation." }, + "configValue": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The invalid config string so the bad experiment value can be identified." } + } + */ + this._telemetryService.sendMSFTTelemetryEvent('incorrectNesGlobalBudgetConfig', { errorMessage: result.err, configValue: configString }); + return undefined; + } + + return result.val; + } + private getEndpointWithLogging(configuredModelName: string | undefined, logContext: InlineEditRequestLogContext, telemetry: StatelessNextEditTelemetryBuilder): ChatEndpoint { const endpoint = this.getEndpoint(configuredModelName); logContext.setEndpointInfo(typeof endpoint.urlOrRequestMetadata === 'string' ? endpoint.urlOrRequestMetadata : JSON.stringify(endpoint.urlOrRequestMetadata.type), endpoint.model); diff --git a/extensions/copilot/src/extension/xtab/test/common/promptCrafting.spec.ts b/extensions/copilot/src/extension/xtab/test/common/promptCrafting.spec.ts index ccae96a0a982b..9deffc6eaed5f 100644 --- a/extensions/copilot/src/extension/xtab/test/common/promptCrafting.spec.ts +++ b/extensions/copilot/src/extension/xtab/test/common/promptCrafting.spec.ts @@ -8,6 +8,8 @@ import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/do import { Edits } from '../../../../platform/inlineEdits/common/dataTypes/edit'; import { LanguageId } from '../../../../platform/inlineEdits/common/dataTypes/languageId'; import { AggressivenessLevel, CurrentFileOptions, DEFAULT_OPTIONS, GlobalBudgetOptions, IncludeLineNumbersOption, PromptingStrategy, PromptOptions } from '../../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; +import { LanguageContextResponse } from '../../../../platform/inlineEdits/common/dataTypes/languageContext'; +import { ContextKind } from '../../../../platform/languageServer/common/languageContextService'; import { StatelessNextEditDocument } from '../../../../platform/inlineEdits/common/statelessNextEditProvider'; import { TestLanguageDiagnosticsService } from '../../../../platform/languages/common/testLanguageDiagnosticsService'; import { Result } from '../../../../util/common/result'; @@ -16,8 +18,9 @@ import { StringEdit } from '../../../../util/vs/editor/common/core/edits/stringE import { Position } from '../../../../util/vs/editor/common/core/position'; import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange'; import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText'; +import { Uri } from '../../../../vscodeTypes'; import { LintErrors } from '../../common/lintErrors'; -import { constructTaggedFile, createTaggedCurrentFileContentUsingPagedClipping, expandRangeToPageRange, getUserPrompt, PromptPieces } from '../../common/promptCrafting'; +import { constructTaggedFile, createTaggedCurrentFileContentUsingPagedClipping, expandRangeToPageRange, getUserPrompt, PromptPieces, runGlobalBudgetCascade } from '../../common/promptCrafting'; import { PromptTags } from '../../common/tags'; import { CurrentDocument } from '../../common/xtabCurrentDocument'; @@ -839,7 +842,7 @@ describe('getUserPrompt — globalBudget cascade', () => { return { activeDoc, currentDocument, currentDocLines }; } - function makePieces(globalBudget: PromptOptions['globalBudget']): PromptPieces { + function makePieces(globalBudget: PromptOptions['globalBudget'], extra?: { langCtx?: LanguageContextResponse; precomputedCascade?: ReturnType }): PromptPieces { const { activeDoc, currentDocument, currentDocLines } = makeActiveDoc(); const promptOptions: PromptOptions = { ...DEFAULT_OPTIONS, @@ -854,14 +857,28 @@ describe('getUserPrompt — globalBudget cascade', () => { [], currentDocLines, 'some code', - undefined, + extra?.langCtx, AggressivenessLevel.Medium, new LintErrors(activeDoc.id, currentDocument, new TestLanguageDiagnosticsService()), s => Math.ceil(s.length / 4), promptOptions, + undefined, + extra?.precomputedCascade, ); } + function makeLangCtxWithSnippet(value: string): LanguageContextResponse { + return { + start: 0, + end: 0, + items: [{ + context: { kind: ContextKind.Snippet, priority: 1, uri: Uri.parse('file:///test/ctx.ts'), value }, + timeStamp: 0, + onTimeout: false, + }], + }; + } + test('produces the same prompt as legacy path when budgets are large', () => { const legacy = getUserPrompt(makePieces(undefined)); const cascaded = getUserPrompt(makePieces({ @@ -898,6 +915,7 @@ describe('getUserPrompt — globalBudget cascade', () => { totalTokens: 1000, order: GlobalBudgetOptions.DEFAULT_ORDER, shares: { + currentFile: 0.5, recentlyViewedDocuments: 0.5, languageContext: 0.5, neighborFiles: 0.5, @@ -913,11 +931,79 @@ describe('getUserPrompt — globalBudget cascade', () => { order: GlobalBudgetOptions.DEFAULT_ORDER, // missing 'diffHistory' shares: { + currentFile: 0.2, languageContext: 0.4, - recentlyViewedDocuments: 0.4, + recentlyViewedDocuments: 0.2, neighborFiles: 0.2, - } as Record<'languageContext' | 'recentlyViewedDocuments' | 'neighborFiles' | 'diffHistory', number>, + } as Record<'currentFile' | 'languageContext' | 'recentlyViewedDocuments' | 'neighborFiles' | 'diffHistory', number>, }); expect(() => getUserPrompt(pieces)).toThrow(/shares is missing entry for 'diffHistory'/); }); + + test('throws when shares is missing an entry for currentFile', () => { + const pieces = makePieces({ + totalTokens: 1000, + order: GlobalBudgetOptions.DEFAULT_ORDER, + // missing 'currentFile' + shares: { + languageContext: 0.25, + recentlyViewedDocuments: 0.25, + neighborFiles: 0.25, + diffHistory: 0.25, + } as Record<'currentFile' | 'languageContext' | 'recentlyViewedDocuments' | 'neighborFiles' | 'diffHistory', number>, + }); + expect(() => getUserPrompt(pieces)).toThrow(/shares is missing entry for 'currentFile'/); + }); + + function runCascade(globalBudget: GlobalBudgetOptions, extra?: { langCtx?: LanguageContextResponse }) { + const { activeDoc } = makeActiveDoc(); + const opts: PromptOptions = { ...DEFAULT_OPTIONS, globalBudget }; + return runGlobalBudgetCascade(activeDoc, [], extra?.langCtx, s => Math.ceil(s.length / 4), opts, undefined, globalBudget); + } + + test('finalSurplus carries the full unused pool when no cascade part consumes budget', () => { + // No langCtx, empty history and neighbors disabled ⇒ every cascade part + // consumes 0, so the entire non-currentFile pool carries to finalSurplus. + // DEFAULT_TOTAL_TOKENS (7500) − currentFileBudget (1500) = 6000. This is + // exactly the budget the provider adds to the current file's clip + // (currentFileBudget 1500 + finalSurplus 6000 = 7500 = T). + const cascade = runCascade({ + totalTokens: GlobalBudgetOptions.DEFAULT_TOTAL_TOKENS, + order: GlobalBudgetOptions.DEFAULT_ORDER, + shares: GlobalBudgetOptions.DEFAULT_SHARES, + }); + expect(cascade.finalSurplus).toBe(6000); + }); + + test('finalSurplus shrinks by what the cascade consumes, so the current file reuses less leftover', () => { + const globalBudget: GlobalBudgetOptions = { + totalTokens: GlobalBudgetOptions.DEFAULT_TOTAL_TOKENS, + order: GlobalBudgetOptions.DEFAULT_ORDER, + shares: GlobalBudgetOptions.DEFAULT_SHARES, + }; + // Empty cascade ⇒ the whole non-currentFile pool (6000) carries to finalSurplus. + const empty = runCascade(globalBudget); + // A rendered language-context snippet consumes budget from the pool, so less + // leftover carries to finalSurplus. The provider clips the current file last to + // currentFileBudget + finalSurplus, so a smaller finalSurplus ⇒ the current file + // reuses less leftover (it is trimmed more) once other parts have content. + const consuming = runCascade(globalBudget, { langCtx: makeLangCtxWithSnippet('const ctxMarker = 1;\n'.repeat(40)) }); + expect(consuming.finalSurplus).toBeLessThan(empty.finalSurplus); + }); + + test('precomputedCascade produces an identical prompt to computing the cascade internally', () => { + const snippet = 'const sharedCtxMarker = 42;'; + const globalBudget: PromptOptions['globalBudget'] = { + totalTokens: 100000, + order: GlobalBudgetOptions.DEFAULT_ORDER, + shares: GlobalBudgetOptions.DEFAULT_SHARES, + }; + const internal = getUserPrompt(makePieces(globalBudget, { langCtx: makeLangCtxWithSnippet(snippet) })); + + const cascade = runCascade(globalBudget, { langCtx: makeLangCtxWithSnippet(snippet) }); + const precomputed = getUserPrompt(makePieces(globalBudget, { langCtx: makeLangCtxWithSnippet(snippet), precomputedCascade: cascade })); + + expect(precomputed.prompt).toBe(internal.prompt); + expect(precomputed.nDiffsInPrompt).toBe(internal.nDiffsInPrompt); + }); }); diff --git a/extensions/copilot/src/extension/xtab/test/node/xtabCustomDiffPatchResponseHandler.spec.ts b/extensions/copilot/src/extension/xtab/test/node/xtabPatchResponseHandler.spec.ts similarity index 83% rename from extensions/copilot/src/extension/xtab/test/node/xtabCustomDiffPatchResponseHandler.spec.ts rename to extensions/copilot/src/extension/xtab/test/node/xtabPatchResponseHandler.spec.ts index 72520d3743102..44b190c86cdff 100644 --- a/extensions/copilot/src/extension/xtab/test/node/xtabCustomDiffPatchResponseHandler.spec.ts +++ b/extensions/copilot/src/extension/xtab/test/node/xtabPatchResponseHandler.spec.ts @@ -1087,4 +1087,233 @@ another_file.js: expect(edits[0].edit).toEqual(new LineReplacement(new LineRange(2, 3), [' let x = 1;'])); }); }); + + describe('splitReplacement', () => { + + it('splits a multi-hunk replacement into one replacement per changed region', () => { + // removed lines 2..6 (1-based), only lines 3 ("b") and 5 ("d") change. + const removedLines = ['a', 'b', 'c', 'd', 'e']; + const replacement = new LineReplacement(new LineRange(2, 7), ['a', 'B', 'c', 'D', 'e']); + + const result = XtabPatchResponseHandler.splitReplacement(replacement, removedLines); + + expect(result).toEqual([ + new LineReplacement(new LineRange(3, 4), ['B']), + new LineReplacement(new LineRange(5, 6), ['D']), + ]); + }); + + it('leaves a single contiguous change unsplit', () => { + const removedLines = ['a', 'b', 'c']; + const replacement = new LineReplacement(new LineRange(10, 13), ['a', 'X', 'Y', 'c']); + + const result = XtabPatchResponseHandler.splitReplacement(replacement, removedLines); + + expect(result).toEqual([replacement]); + }); + + it('leaves a pure insertion unsplit', () => { + const replacement = new LineReplacement(new LineRange(5, 5), ['x', 'y']); + + const result = XtabPatchResponseHandler.splitReplacement(replacement, []); + + expect(result).toEqual([replacement]); + }); + + it('leaves a pure deletion unsplit', () => { + const replacement = new LineReplacement(new LineRange(5, 8), []); + + const result = XtabPatchResponseHandler.splitReplacement(replacement, ['x', 'y', 'z']); + + expect(result).toEqual([replacement]); + }); + }); + + describe('handleResponse with splitPatchOnDiff', () => { + + const docContent = '0\na\nb\nc\nd\ne\n'; + + // A single patch block whose only real changes are line "b"->"B" and + // "d"->"D"; the lines in between are re-emitted context. + async function* makeStream(): AsyncGenerator { + yield '/test.ts:1'; + yield '-a'; + yield '-b'; + yield '-c'; + yield '-d'; + yield '-e'; + yield '+a'; + yield '+B'; + yield '+c'; + yield '+D'; + yield '+e'; + } + + it('splits one coarse patch into minimal replacements when enabled', async () => { + const docId = DocumentId.create('file:///test.ts'); + const documentBeforeEdits = new CurrentDocument(new StringText(docContent), new Position(1, 1)); + + const { edits } = await consumeHandleResponse( + makeStream(), + documentBeforeEdits, + docId, + undefined, + undefined, + new TestLogService(), + DuplicateAdditionsMode.Off, + false, + true, + ); + + expect(edits.map(e => e.edit)).toEqual([ + new LineReplacement(new LineRange(3, 4), ['B']), + new LineReplacement(new LineRange(5, 6), ['D']), + ]); + }); + + it('yields a single coarse replacement when disabled (default)', async () => { + const docId = DocumentId.create('file:///test.ts'); + const documentBeforeEdits = new CurrentDocument(new StringText(docContent), new Position(1, 1)); + + const { edits } = await consumeHandleResponse( + makeStream(), + documentBeforeEdits, + docId, + undefined, + undefined, + new TestLogService(), + ); + + expect(edits.map(e => e.edit)).toEqual([ + new LineReplacement(new LineRange(2, 7), ['a', 'B', 'c', 'D', 'e']), + ]); + }); + }); + + describe('patchIndex attribution', () => { + + it('extractEdits assigns an incrementing index per model patch header', async () => { + const linesStream = AsyncIterUtils.fromArray([ + 'a.ts:1', + '-a', + '+A', + 'b.ts:2', + '-b', + '+B', + 'c.ts:3', + '-c', + '+C', + ]); + const patches = await AsyncIterUtils.toArray(XtabPatchResponseHandler.extractEdits(linesStream)); + expect(patches.map(p => p.patchIndex)).toEqual([0, 1, 2]); + }); + + it('extractEdits does not advance the index for an invalid header', async () => { + const linesStream = AsyncIterUtils.fromArray([ + 'a.ts:1', + '-a', + '+A', + 'not a valid header', + 'b.ts:2', + '-b', + '+B', + ]); + const patches = await AsyncIterUtils.toArray(XtabPatchResponseHandler.extractEdits(linesStream)); + // The invalid header is skipped, so the two valid patches remain 0 and 1. + expect(patches.map(p => p.patchIndex)).toEqual([0, 1]); + }); + + it('all fragments of a split patch share the originating patchIndex', async () => { + const docId = DocumentId.create('file:///test.ts'); + const docContent = '0\na\nb\nc\nd\ne\n'; + const documentBeforeEdits = new CurrentDocument(new StringText(docContent), new Position(1, 1)); + + async function* makeStream(): AsyncGenerator { + yield '/test.ts:1'; + yield '-a'; + yield '-b'; + yield '-c'; + yield '-d'; + yield '-e'; + yield '+a'; + yield '+B'; + yield '+c'; + yield '+D'; + yield '+e'; + } + + const { edits } = await consumeHandleResponse( + makeStream(), + documentBeforeEdits, + docId, + undefined, + undefined, + new TestLogService(), + DuplicateAdditionsMode.Off, + false, + true, + ); + + expect(edits).toHaveLength(2); + expect(edits.map(e => e.patchIndex)).toEqual([0, 0]); + }); + + it('distinct model patches get distinct patchIndex values', async () => { + const docId = DocumentId.create('file:///test.ts'); + const docContent = '0\na\nb\nc\nd\n'; + const documentBeforeEdits = new CurrentDocument(new StringText(docContent), new Position(1, 1)); + + async function* makeStream(): AsyncGenerator { + yield '/test.ts:1'; + yield '-a'; + yield '+A'; + yield '/test.ts:3'; + yield '-c'; + yield '+C'; + } + + const { edits } = await consumeHandleResponse( + makeStream(), + documentBeforeEdits, + docId, + undefined, + undefined, + new TestLogService(), + ); + + expect(edits.map(e => e.patchIndex)).toEqual([0, 1]); + }); + + it('progressive ghost-text fragments share the patchIndex while a following patch advances it', async () => { + const docId = DocumentId.create('file:///test.ts'); + const docContent = 'function foo() {\n let x = 1;\n}\nbar();\n'; + // Cursor on line 2 (1-based) → cursorLineOffset = 1 + const documentBeforeEdits = new CurrentDocument(new StringText(docContent), new Position(2, 5)); + + async function* makeStream(): AsyncGenerator { + yield '/test.ts:1'; + yield '- let x = 1;'; + yield '+ let x = 1;'; + yield '+ let y = 2;'; + yield '/test.ts:3'; + yield '-bar();'; + yield '+baz();'; + } + + const { edits } = await consumeHandleResponse( + makeStream(), + documentBeforeEdits, + docId, + undefined, + undefined, + new TestLogService(), + DuplicateAdditionsMode.Off, + true, + ); + + // First two edits are the ghost-text early + continuation of patch 0; + // the third edit comes from the second model patch. + expect(edits.map(e => e.patchIndex)).toEqual([0, 0, 1]); + }); + }); }); diff --git a/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts b/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts index 00253ca1e0a49..82acfbff68381 100644 --- a/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts +++ b/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts @@ -22,8 +22,10 @@ import { ILogger } from '../../../../platform/log/common/logService'; import { FilterReason } from '../../../../platform/networking/common/openai'; import { ISimulationTestContext } from '../../../../platform/simulationTestContext/common/simulationTestContext'; import { TestLogService } from '../../../../platform/testing/common/testLogService'; +import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService'; import { AsyncIterUtils } from '../../../../util/common/asyncIterableUtils'; import { Result } from '../../../../util/common/result'; +import { createTextDocumentData } from '../../../../util/common/test/shims/textDocument'; import { DeferredPromise } from '../../../../util/vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from '../../../../util/vs/base/common/cancellation'; import { Emitter, Event } from '../../../../util/vs/base/common/event'; @@ -40,6 +42,7 @@ import { DelaySession } from '../../../inlineEdits/common/delay'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { N_LINES_AS_CONTEXT } from '../../common/promptCrafting'; import { nes41Miniv3SystemPrompt, simplifiedPrompt, systemPromptTemplate, unifiedModelSystemPrompt, xtab275SystemPrompt } from '../../common/systemMessages'; +import { PromptTags } from '../../common/tags'; import { CurrentDocument } from '../../common/xtabCurrentDocument'; import { computeAreaAroundEditWindowLinesRange, @@ -1136,6 +1139,69 @@ describe('XtabProvider integration', () => { // Group 4: Filter Pipeline // ======================================================================== + describe('global budget', () => { + + /** Drives the provider once and returns the captured user-message text. */ + async function captureUserPrompt(provider: XtabProvider, request: StatelessNextEditRequest): Promise { + streamingFetcher.setStreamingLines(['x']); + const capturesBefore = streamingFetcher.capturedOptions.length; + const gen = provider.provideNextEdit(request, createMockLogger(), createLogContext(), CancellationToken.None); + await AsyncIterUtils.drainUntilReturn(gen); + // Guard against silently comparing a stale capture: the run must have fetched. + expect(streamingFetcher.capturedOptions.length).toBeGreaterThan(capturesBefore); + const messages = streamingFetcher.capturedOptions.at(-1)?.messages; + const userMessage = messages?.find(m => m.role === Raw.ChatRole.User); + expect(userMessage).toBeDefined(); + return getMessageText(userMessage!); + } + + /** Number of lines in the `<|current_file_content|>` region of the prompt. */ + function currentFileRegionLineCount(prompt: string): number { + const start = prompt.indexOf(PromptTags.CURRENT_FILE.start); + const end = prompt.indexOf(PromptTags.CURRENT_FILE.end); + expect(start).toBeGreaterThanOrEqual(0); + expect(end).toBeGreaterThan(start); + return prompt.slice(start, end).split('\n').length; + } + + const bigFile = Array.from({ length: 400 }, (_, i) => `const value${i} = ${i};`); + + // Under a global budget the current file is clipped LAST, so it absorbs whatever + // budget the cascade parts leave unused. With the (here empty) cascade the + // current file therefore reuses essentially the whole pool and keeps strictly + // MORE of the file than the legacy path, which caps it at its own + // currentFile.maxTokens (1500) and trims the tail. + it('absorbs leftover cascade budget so it keeps more of the current file than the legacy cap', async () => { + const legacy = await captureUserPrompt(createProvider(), createRequestWithEdit(bigFile, { insertionOffset: 3, insertedText: 'a' })); + + await configService.setConfig(ConfigKey.TeamInternal.InlineEditsXtabGlobalBudget, '{}'); + const enabledAtDefault = await captureUserPrompt(createProvider(), createRequestWithEdit(bigFile, { insertionOffset: 3, insertedText: 'a' })); + + // Legacy caps the current file at 2000 tokens → the tail is trimmed. + expect(legacy).not.toContain('const value399 = 399;'); + // Clip-last lets the current file reuse the whole pool → the entire file fits. + expect(enabledAtDefault).toContain('const value399 = 399;'); + expect(currentFileRegionLineCount(legacy)).toBeLessThan(currentFileRegionLineCount(enabledAtDefault)); + }); + + // New behavior: because the current file is sized to its share PLUS the cascade + // leftover (≈ the whole pool when the cascade is empty), a larger total budget + // keeps more of the file. A small pool still trims the tail; a generous pool + // fits the entire file. + it('keeps more of the current file as the total budget grows', async () => { + await configService.setConfig(ConfigKey.TeamInternal.InlineEditsXtabGlobalBudget, JSON.stringify({ totalTokens: 2000 })); + const smallBudget = await captureUserPrompt(createProvider(), createRequestWithEdit(bigFile, { insertionOffset: 3, insertedText: 'a' })); + + await configService.setConfig(ConfigKey.TeamInternal.InlineEditsXtabGlobalBudget, JSON.stringify({ totalTokens: 8000 })); + const wideBudget = await captureUserPrompt(createProvider(), createRequestWithEdit(bigFile, { insertionOffset: 3, insertedText: 'a' })); + + // Small pool trims the tail; wide pool fits the whole file. + expect(smallBudget).not.toContain('const value399 = 399;'); + expect(wideBudget).toContain('const value399 = 399;'); + expect(currentFileRegionLineCount(smallBudget)).toBeLessThan(currentFileRegionLineCount(wideBudget)); + }); + }); + describe('filter pipeline', () => { it('filters out import-only changes', async () => { const provider = createProvider(); @@ -1770,6 +1836,59 @@ describe('XtabProvider integration', () => { expect(streamingFetcher.callCount).toBe(3); }); + it('cross-file cursor jump with out-of-bounds predicted line → NoSuggestions without throwing', async () => { + const provider = createProvider(); + await configService.setConfig(ConfigKey.InlineEditsNextCursorPredictionEnabled, true); + await configService.setConfig(ConfigKey.TeamInternal.InlineEditsNextCursorPredictionModelName, 'test-model'); + + const lines = Array.from({ length: 30 }, (_, i) => `line ${i} content`); + const cursorOffset = lines.slice(0, 5).join('\n').length; + const request = createRequestWithEdit(lines, { insertionOffset: cursorOffset }); + + // 1st call (main LLM): edit-window lines unchanged → no edits → cursor jump path + const mainEditWindowLines = lines.slice(3, 11); + streamingFetcher.enqueueResponse({ + type: ChatFetchResponseType.Success, + requestId: 'req-main', + serverRequestId: 'srv-main', + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, prompt_tokens_details: { cached_tokens: 0 } }, + value: mainEditWindowLines.join('\n'), + resolvedModel: 'test-model', + }); + + // 2nd call (cursor prediction): predict a jump into another file at line 50 (0-based), + // which is out of bounds for the 5-line target document opened below. + streamingFetcher.enqueueResponse({ + type: ChatFetchResponseType.Success, + requestId: 'req-cursor', + serverRequestId: 'srv-cursor', + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, prompt_tokens_details: { cached_tokens: 0 } }, + value: '/test/other.ts:50', + resolvedModel: 'test-model', + }); + + // The cross-file jump target only has 5 lines, so the predicted line 50 is out of bounds. + const targetDoc = createTextDocumentData( + URI.file('/test/other.ts'), + Array.from({ length: 5 }, (_, i) => `other ${i}`).join('\n'), + 'typescript', + ).document; + const workspaceService = instaService.invokeFunction(accessor => accessor.get(IWorkspaceService)); + const openSpy = vi.spyOn(workspaceService, 'openTextDocument').mockResolvedValue(targetDoc); + + const gen = provider.provideNextEdit(request, createMockLogger(), createLogContext(), CancellationToken.None); + const { edits, finalReason } = await collectEdits(gen); + + expect(edits.length).toBe(0); + expect(finalReason.v).toBeInstanceOf(NoNextEditReason.NoSuggestions); + // The out-of-bounds prediction must be rejected before any retry fetch happens, rather + // than constructing a CurrentDocument with an out-of-bounds cursor (which would throw). + expect(streamingFetcher.callCount).toBe(2); + expect(openSpy).toHaveBeenCalledOnce(); + + openSpy.mockRestore(); + }); + it('model fallback retry on NotFound then yields edits on second attempt', async () => { const provider = createProvider(); diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 548e08874627e..462d3dd09d1f2 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -728,8 +728,12 @@ export namespace ConfigKey { // OTel settings export const OTelEnabled = defineSetting('chat.otel.enabled', ConfigType.Simple, false); export const OTelExporterType = defineSetting('chat.otel.exporterType', ConfigType.Simple, 'otlp-http'); + export const OTelProtocol = defineSetting('chat.otel.protocol', ConfigType.Simple, ''); export const OTelOtlpEndpoint = defineSetting('chat.otel.otlpEndpoint', ConfigType.Simple, 'http://localhost:4318'); export const OTelCaptureContent = defineSetting('chat.otel.captureContent', ConfigType.Simple, false); + export const OTelServiceName = defineSetting('chat.otel.serviceName', ConfigType.Simple, ''); + export const OTelResourceAttributes = defineSetting>('chat.otel.resourceAttributes', ConfigType.Simple, {}); + export const OTelHeaders = defineSetting>('chat.otel.headers', ConfigType.Simple, {}); export const OTelMaxAttributeSizeChars = defineSetting('chat.otel.maxAttributeSizeChars', ConfigType.Simple, 0); export const OTelOutfile = defineSetting('chat.otel.outfile', ConfigType.Simple, ''); export const OTelDbSpanExporter = defineSetting('chat.otel.dbSpanExporter.enabled', ConfigType.Simple, false); @@ -881,11 +885,11 @@ export namespace ConfigKey { export const InlineEditsXtabLanguageContextMaxTokens = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.languageContext.maxTokens', ConfigType.ExperimentBased, xtabPromptOptions.DEFAULT_OPTIONS.languageContext.maxTokens); export const InlineEditsXtabIncludeNeighborFiles = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.neighborFiles.enabled', ConfigType.ExperimentBased, xtabPromptOptions.DEFAULT_OPTIONS.neighborFiles.enabled); export const InlineEditsXtabNeighborFilesMaxTokens = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.neighborFiles.maxTokens', ConfigType.ExperimentBased, xtabPromptOptions.DEFAULT_OPTIONS.neighborFiles.maxTokens); - export const InlineEditsXtabGlobalBudgetEnabled = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.globalBudget.enabled', ConfigType.ExperimentBased, false); - export const InlineEditsXtabGlobalBudgetTotalTokens = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.globalBudget.totalTokens', ConfigType.ExperimentBased, xtabPromptOptions.GlobalBudgetOptions.DEFAULT_TOTAL_TOKENS); + export const InlineEditsXtabGlobalBudget = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.globalBudget', ConfigType.ExperimentBased, undefined); export const InlineEditsXtabMaxMergeConflictLines = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.maxMergeConflictLines', ConfigType.ExperimentBased, undefined); export const InlineEditsXtabOnlyMergeConflictLines = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.onlyMergeConflictLines', ConfigType.ExperimentBased, false); export const InlineEditsXtabDuplicateAdditionsMode = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.diffPatch.duplicateAdditionsMode', ConfigType.ExperimentBased, DuplicateAdditionsMode.Off, DuplicateAdditionsMode.VALIDATOR); + export const InlineEditsXtabSplitPatchOnDiff = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.diffPatch.splitOnDiff', ConfigType.ExperimentBased, false, vBoolean()); export const InlineEditsXtabAggressivenessLevel = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.aggressivenessLevel', ConfigType.ExperimentBased, undefined); export const InlineEditsAggressivenessLowMinResponseTimeMs = defineTeamInternalSetting('chat.advanced.inlineEdits.aggressiveness.lowMinResponseTimeMs', ConfigType.ExperimentBased, 1500); export const InlineEditsAggressivenessMediumMinResponseTimeMs = defineTeamInternalSetting('chat.advanced.inlineEdits.aggressiveness.mediumMinResponseTimeMs', ConfigType.ExperimentBased, 700); @@ -899,15 +903,6 @@ export namespace ConfigKey { export const InlineEditsJointCompletionsProviderTriggerChangeStrategy = defineTeamInternalSetting('chat.advanced.inlineEdits.jointCompletionsProvider.triggerChangeStrategy', ConfigType.ExperimentBased, JointCompletionsProviderTriggerChangeStrategy.NoTriggerOnCompletionsRequestInFlight); export const InstantApplyModelName = defineTeamInternalSetting('chat.advanced.instantApply.modelName', ConfigType.ExperimentBased, CHAT_MODEL.GPT4OPROXY); export const VerifyTextDocumentChanges = defineTeamInternalSetting('chat.advanced.inlineEdits.verifyTextDocumentChanges', ConfigType.ExperimentBased, false); - export const UseAutoModeRouting = defineTeamInternalSetting('chat.advanced.useAutoModeRouter', ConfigType.ExperimentBased, false); - /** Controls which `routing_method` value is sent to the auto-intent-service per request - * when `UseAutoModeRouting` is enabled. - * '' (empty/default) = omit `routing_method` and use the server default. - * 'binary' = binary classifier v1. - * 'hydra' = HYDRA multi-head capability matching. - * For experiments, this setting selects the routing method only when router usage is enabled; - * it does not by itself determine whether the router is called. */ - export const AutoModeRoutingMethod = defineTeamInternalSetting('chat.advanced.autoModeRoutingMethod', ConfigType.ExperimentBased, '', undefined, undefined, { experimentName: 'copilotchat.autoModeRoutingMethod' }); /** Inline Completions */ export const InlineCompletionsDefaultDiagnosticsOptions = defineTeamInternalSetting('chat.advanced.inlineCompletions.defaultDiagnosticsOptionsString', ConfigType.ExperimentBased, undefined); @@ -968,14 +963,12 @@ export namespace ConfigKey { export const UseAnthropicMessagesApi = defineSetting('chat.anthropic.useMessagesApi', ConfigType.ExperimentBased, true); /** Context editing mode for Anthropic Messages API. 'off' disables context editing. */ export const AnthropicContextEditingMode = defineSetting<'off' | 'clear-thinking' | 'clear-tooluse' | 'clear-both'>('chat.anthropic.contextEditing.mode', ConfigType.ExperimentBased, 'off'); - /** Configure reasoning summary style sent to Responses API */ - export const ResponsesApiReasoningSummary = defineSetting<'off' | 'detailed'>('chat.responsesApiReasoningSummary', ConfigType.ExperimentBased, 'detailed'); /** Enable context_management sent to Responses API */ export const ResponsesApiContextManagementEnabled = defineSetting('chat.responsesApiContextManagement.enabled', ConfigType.ExperimentBased, false); /** Enable client-side prompt_cache_key (conversationId:modelFamily) sent to Responses API */ export const ResponsesApiPromptCacheKeyEnabled = defineSetting('chat.responsesApi.promptCacheKey.enabled', ConfigType.ExperimentBased, false); - /** Enable persistent chain of thought for supported Responses API model families */ - export const ResponsesApiPersistentCoTEnabled = defineSetting('chat.responsesApi.persistentCoT.enabled', ConfigType.ExperimentBased, false); + /** Enable explicit prompt_cache_breakpoint markers sent to Responses API */ + export const ResponsesApiPromptCacheBreakpointEnabled = defineSetting('chat.responsesApi.promptCacheBreakpoint.enabled', ConfigType.ExperimentBased, false); /** Enable updated prompt for 5.3Codex model */ export const Updated53CodexPromptEnabled = defineSetting('chat.updated53CodexPrompt.enabled', ConfigType.ExperimentBased, true); /** Enable updated prompt for Claude Opus 4.7 model */ @@ -983,12 +976,12 @@ export namespace ConfigKey { /** Enable get_changed_files tool for GPT-5.5 models */ export const EnableGpt55GetChangedFilesTool = defineSetting('chat.gpt55GetChangedFilesTool.enabled', ConfigType.ExperimentBased, true); /** Enable get_changed_files tool for Gemini 3 models */ - export const EnableGemini3GetChangedFilesTool = defineSetting('chat.gemini3GetChangedFilesTool.enabled', ConfigType.ExperimentBased, true); + export const EnableGemini3GetChangedFilesTool = defineSetting('chat.gemini3GetChangedFilesTool.enabled', ConfigType.ExperimentBased, false); /** When enabled, sends `reasoning_effort: 'low'` to Gemini 3 models. */ export const EnableGemini3LowReasoningEffort = defineSetting('chat.gemini3LowReasoningEffort.enabled', ConfigType.ExperimentBased, false); /** Enable read_file tool for GPT-5.5 models */ export const EnableGpt55ReadFileTool = defineSetting('chat.gpt55ReadFileTool.enabled', ConfigType.ExperimentBased, true); - export const EnableChatImageUpload = defineSetting('chat.imageUpload.enabled', ConfigType.ExperimentBased, true); + export const EnableChatImageUpload = defineSetting('chat.imageUpload.enabled', ConfigType.Simple, true); /** Enable Anthropic web search tool for BYOK Claude models */ export const AnthropicWebSearchToolEnabled = defineSetting('chat.anthropic.tools.websearch.enabled', ConfigType.ExperimentBased, false); /** Maximum number of web searches allowed per request */ @@ -1057,7 +1050,7 @@ export namespace ConfigKey { export const EnableAlternateGptPrompt = defineSetting('chat.alternateGptPrompt.enabled', ConfigType.ExperimentBased, false); export const EnableAlternateGeminiModelFPrompt = defineSetting('chat.alternateGeminiModelFPrompt.enabled', ConfigType.ExperimentBased, false); - export const EnableGemini35FlashReducedToolUsePrompt = defineSetting('chat.gemini35FlashReducedToolUsePrompt.enabled', ConfigType.ExperimentBased, false); + export const EnableGemini35FlashReducedToolUsePrompt = defineSetting('chat.gemini35FlashReducedToolUsePrompt.enabled', ConfigType.ExperimentBased, true); export const EnableOrganizationCustomAgents = defineSetting('chat.organizationCustomAgents.enabled', ConfigType.Simple, true); export const EnableOrganizationInstructions = defineSetting('chat.organizationInstructions.enabled', ConfigType.Simple, true); @@ -1100,7 +1093,9 @@ export namespace ConfigKey { export const LocalIndexEnabled = defineSetting('chat.localIndex.enabled', ConfigType.ExperimentBased, false); /** grep_search configs */ - export const GrepSearchOutputFormat = defineSetting<'grep' | 'tag'>('chat.tools.grepSearch.outputFormat', ConfigType.ExperimentBased, 'tag'); + export const GrepSearchOutputFormat = defineSetting<'grep' | 'tag'>('chat.tools.grepSearch.outputFormat', ConfigType.ExperimentBased, 'grep'); + export const GrepSearchDefaultMaxResults = defineSetting('chat.tools.grepSearch.defaultMaxResults', ConfigType.ExperimentBased, 20); + export const GrepSearchMaxResultsCap = defineSetting('chat.tools.grepSearch.maxResultsCap', ConfigType.ExperimentBased, 200); } export function getAllConfigKeys(): string[] { diff --git a/extensions/copilot/src/platform/endpoint/common/chatModelCapabilities.ts b/extensions/copilot/src/platform/endpoint/common/chatModelCapabilities.ts index fb725ff704d3c..c633084a0daff 100644 --- a/extensions/copilot/src/platform/endpoint/common/chatModelCapabilities.ts +++ b/extensions/copilot/src/platform/endpoint/common/chatModelCapabilities.ts @@ -38,6 +38,8 @@ const VSC_MODEL_HASHES_D = [ 'e82ff0e2d4e4bae1f012dc599d520f8d61becfc4762f3717577b270be199db92', ]; +const VSC_MODEL_HASHES_E: string[] = []; + // subset to allow replace string instead of apply patch. const VSC_MODEL_HASHES_EDIT_TOOL_SET = [ @@ -71,10 +73,6 @@ const HIDDEN_FAMILY_H_HASHES: string[] = [ '70fcded3f255d368e868cc807d8838a62108bfa5c86ce7d37966f58cda229e33', ]; -const HIDDEN_FAMILY_M_HASHES: string[] = [ - '0902565c0c0fe145633a1f246ae551acc0f621249ef050428eba357fbd4655ee', -]; - /** * Per-model capability override. Lets advanced users (and evals) alias an * unknown/preview model id to a known production family for capability @@ -156,9 +154,18 @@ export function isGpt55(model: LanguageModelChat | IChatEndpoint | string) { return family.startsWith('gpt-5.5') || HIDDEN_MODEL_B_HASHES.includes(h); } -export function isHiddenModelM(model: LanguageModelChat | IChatEndpoint | string) { - const family_hash = getCachedSha256Hash(typeof model === 'string' ? model : model.family); - return HIDDEN_FAMILY_M_HASHES.includes(family_hash); +export function isGpt56(model: LanguageModelChat | IChatEndpoint | string) { + return isGpt56SolOrTerra(model) || isGpt56Luna(model); +} + +export function isGpt56SolOrTerra(model: LanguageModelChat | IChatEndpoint | string) { + const family = typeof model === 'string' ? model : model.family; + return family === 'ember-alpha'; +} + +export function isGpt56Luna(model: LanguageModelChat | IChatEndpoint | string) { + const family = typeof model === 'string' ? model : model.family; + return family === 'opal-alpha'; } export function isGpt53Codex(model: LanguageModelChat | IChatEndpoint | string) { @@ -166,6 +173,19 @@ export function isGpt53Codex(model: LanguageModelChat | IChatEndpoint | string) return family.startsWith('gpt-5.3-codex'); } +export function isKimiFamily(model: LanguageModelChat | IChatEndpoint | string): boolean { + const matches = (value: string): boolean => { + const normalized = value.toLowerCase(); + return normalized.startsWith('kimi-k2.6') || normalized.startsWith('kimi-k2.7-code'); + }; + + if (typeof model === 'string') { + return matches(model); + } + + return matches(model.family) || matches(getModelId(model)); +} + export function isVSCModelA(model: LanguageModelChat | IChatEndpoint) { const ID_hash = getCachedSha256Hash(getModelId(model)); @@ -197,6 +217,13 @@ export function isVSCModelD(model: LanguageModelChat | IChatEndpoint) { return VSC_MODEL_HASHES_D.includes(ID_hash) || VSC_MODEL_HASHES_D.includes(family_hash); } +export function isVSCModelE(model: LanguageModelChat | IChatEndpoint) { + const modelId = getModelId(model); + const ID_hash = getCachedSha256Hash(modelId); + const family_hash = getCachedSha256Hash(model.family); + return model.name.startsWith('vscModelE') || model.family.startsWith('vscModelE') || modelId.startsWith('vscModelE') || VSC_MODEL_HASHES_E.includes(ID_hash) || VSC_MODEL_HASHES_E.includes(family_hash); +} + export function isGpt52CodexFamily(model: LanguageModelChat | IChatEndpoint | string): boolean { const family = typeof model === 'string' ? model : model.family; return family === 'gpt-5.2-codex'; @@ -240,7 +267,7 @@ export function modelSupportsApplyPatch(model: LanguageModelChat | IChatEndpoint || isGpt52Family(model.family) || isGpt54(model) || isHiddenModelB(model) - || isHiddenModelM(model); + || isGpt56(model); } /** @@ -254,21 +281,21 @@ export function modelPrefersJsonNotebookRepresentation(model: LanguageModelChat || isGpt52Family(model.family) || isGpt54(model) || isHiddenModelB(model) - || isHiddenModelM(model); + || isGpt56(model); } /** * Model supports replace_string_in_file as an edit tool. */ export function modelSupportsReplaceString(model: LanguageModelChat | IChatEndpoint): boolean { - return isGeminiFamily(model) || model.family.includes('grok-code') || modelSupportsMultiReplaceString(model) || isHiddenModelF(model) || isMinimaxFamily(model) || isHiddenFamilyH(model); + return isGeminiFamily(model) || model.family.includes('grok-code') || modelSupportsMultiReplaceString(model) || isHiddenModelF(model) || isMinimaxFamily(model) || isHiddenFamilyH(model) || isKimiFamily(model); } /** * Model supports multi_replace_string_in_file as an edit tool. */ export function modelSupportsMultiReplaceString(model: LanguageModelChat | IChatEndpoint): boolean { - return isAnthropicFamily(model) || isHiddenModelE(model) || isVSCModelReplaceStringSet(model) || isMinimaxFamily(model) || isHiddenFamilyH(model); + return isAnthropicFamily(model) || isHiddenModelE(model) || isVSCModelReplaceStringSet(model) || isMinimaxFamily(model) || isHiddenFamilyH(model) || isKimiFamily(model); } /** @@ -276,7 +303,7 @@ export function modelSupportsMultiReplaceString(model: LanguageModelChat | IChat * without needing insert_edit_into_file. */ export function modelCanUseReplaceStringExclusively(model: LanguageModelChat | IChatEndpoint): boolean { - return isAnthropicFamily(model) || model.family.includes('grok-code') || isHiddenModelE(model) || model.family.toLowerCase().includes('gemini-3') || isVSCModelReplaceStringSet(model) || isHiddenModelF(model) || isMinimaxFamily(model) || isHiddenFamilyH(model); + return isAnthropicFamily(model) || model.family.includes('grok-code') || isHiddenModelE(model) || model.family.toLowerCase().includes('gemini-3') || isVSCModelReplaceStringSet(model) || isHiddenModelF(model) || isMinimaxFamily(model) || isHiddenFamilyH(model) || isKimiFamily(model); } /** @@ -305,7 +332,16 @@ export function modelCanUseImageURL(model: LanguageModelChat | IChatEndpoint): b * The model supports native PDF document processing via document content parts. */ export function modelSupportsPDFDocuments(model: LanguageModelChat | IChatEndpoint): boolean { - return isAnthropicFamily(model) || isGpt5PlusFamily(model) || isHiddenModelM(model); + return isAnthropicFamily(model) || isGpt5PlusFamily(model) || isGpt56(model); +} + +/** + * The model supports explicit prompt cache breakpoints via the OpenAI + * Responses API (`prompt_cache_breakpoint`). Scoped to OpenAI (GPT) models + * only, since this is an OpenAI-specific Responses API feature. + */ +export function modelSupportCacheBreakPoints(model: LanguageModelChat | IChatEndpoint): boolean { + return isGpt56(model); } /** @@ -429,10 +465,11 @@ export function getVerbosityForModelSync(model: IChatEndpoint): 'low' | 'medium' export function modelSupportsToolSearch(model: LanguageModelChat | IChatEndpoint | string): boolean { const id = typeof model === 'string' ? model : getModelId(model); const family = typeof model === 'string' ? model : model.family; + const isGpt56Model: boolean = isGpt56(model); const matches = (s: string) => { const n = s.toLowerCase().replace(/\./g, '-'); // OpenAI models with client-side tool search. - if (n === 'gpt-5-4' || n === 'gpt-5-5') { + if (n === 'gpt-5-4' || n === 'gpt-5-5' || isGpt56Model) { return true; } if (!n.startsWith('claude')) { @@ -455,7 +492,7 @@ export function modelSupportsToolSearch(model: LanguageModelChat | IChatEndpoint n === 'claude-opus-4' || n.startsWith('claude-opus-4-1') || n.startsWith('claude-opus-4-2'); return !isPre45; }; - return matches(id) || matches(family) || isHiddenModelM(family); + return matches(id) || matches(family); } /** diff --git a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts index 647b162de87ac..28d7f2283de8e 100644 --- a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts +++ b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts @@ -19,6 +19,11 @@ export type CustomModel = { export type EndpointEditToolName = 'find-replace' | 'multi-find-replace' | 'apply-patch' | 'code-rewrite'; +export interface IChatModelRequestOptions { + temperature?: number | null; + top_p?: number | null; +} + const allEndpointEditToolNames: ReadonlySet = new Set([ 'find-replace', 'multi-find-replace', @@ -115,6 +120,7 @@ export interface IModelAPIResponse { version: string; warning_messages?: { code: string; message: string }[]; info_messages?: { code: string; message: string }[]; + warning_text?: Record; billing?: IModelBilling; model_picker_price_category?: string; model_picker_category?: string; @@ -127,6 +133,7 @@ export type IChatModelInformation = IModelAPIResponse & { capabilities: IChatModelCapabilities; urlOrRequestMetadata?: string | RequestMetadata; requestHeaders?: Readonly>; + modelOptions?: Readonly; zeroDataRetentionEnabled?: boolean; /** * BYOK-only override that forces the body shape used when forwarding the reasoning effort to the model. diff --git a/extensions/copilot/src/platform/endpoint/node/automodeService.ts b/extensions/copilot/src/platform/endpoint/node/automodeService.ts index 94bdd3038092e..bf1ce36b2795e 100644 --- a/extensions/copilot/src/platform/endpoint/node/automodeService.ts +++ b/extensions/copilot/src/platform/endpoint/node/automodeService.ts @@ -11,7 +11,6 @@ import { Disposable, DisposableMap } from '../../../util/vs/base/common/lifecycl import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ChatLocation } from '../../../vscodeTypes'; import { IAuthenticationService } from '../../authentication/common/authentication'; -import { ConfigKey, IConfigurationService } from '../../configuration/common/configurationService'; import { IEnvService } from '../../env/common/envService'; import { getImageTelemetryEventMeasurements, getImageTelemetryMeasurementsFromReferences, type ImageTelemetryMeasurements } from '../../image/common/imageTelemetry'; import { ILogService } from '../../log/common/logService'; @@ -104,6 +103,13 @@ class AutoModeTokenBank extends Disposable { } } +export interface AutoModeRoutingDecision { + resolvedModel: string; + resolvedModelName: string; + predictedLabel: 'needs_reasoning' | 'no_reasoning' | 'fallback'; + confidence: number; +} + export const IAutomodeService = createServiceIdentifier('IAutomodeService'); export interface IAutomodeService { @@ -111,6 +117,13 @@ export interface IAutomodeService { resolveAutoModeEndpoint(chatRequest: ChatRequest | undefined, knownEndpoints: IChatEndpoint[]): Promise