diff --git a/.github/rulesets/docs-internal/main-branch-protection.json b/.github/rulesets/docs-internal/main-branch-protection.json
index 29ed7c5ca4df..a51eabfb9267 100644
--- a/.github/rulesets/docs-internal/main-branch-protection.json
+++ b/.github/rulesets/docs-internal/main-branch-protection.json
@@ -196,6 +196,10 @@
"context": "shielding",
"integration_id": 15368
},
+ {
+ "context": "tools",
+ "integration_id": 15368
+ },
{
"context": "versions",
"integration_id": 15368
diff --git a/.github/rulesets/docs/main-branch-protection.json b/.github/rulesets/docs/main-branch-protection.json
index 4444e4cc6967..cbec8a04f3c5 100644
--- a/.github/rulesets/docs/main-branch-protection.json
+++ b/.github/rulesets/docs/main-branch-protection.json
@@ -173,6 +173,10 @@
"context": "shielding",
"integration_id": 15368
},
+ {
+ "context": "tools",
+ "integration_id": 15368
+ },
{
"context": "versions",
"integration_id": 15368
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a9f04b656d80..83dc6a78588c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -69,7 +69,7 @@ jobs:
- secret-scanning
- shielding
# - tests
- # - tools
+ - tools
# - types
- versions
- webhooks
@@ -85,7 +85,7 @@ jobs:
env:
# These suites make no HTTP requests, so they skip the Next.js build and test server.
# A suite that adds an HTTP test fails with a connection error until it's removed from this list.
- START_VITEST_SERVER: ${{ contains(fromJSON('["color-schemes", "content-linter", "data-directory", "ghes-releases", "journeys", "links", "observability", "products", "workflows"]'), matrix.name) && 'false' || 'true' }}
+ START_VITEST_SERVER: ${{ contains(fromJSON('["color-schemes", "content-linter", "data-directory", "ghes-releases", "journeys", "links", "observability", "products", "tools", "workflows"]'), matrix.name) && 'false' || 'true' }}
steps:
# Repeat each if on its step so skipped work still leaves the required check present.
diff --git a/content/code-security/reference/secret-security/secret-types.md b/content/code-security/reference/secret-security/secret-types.md
index 0db20172de75..7994fa61e53b 100644
--- a/content/code-security/reference/secret-security/secret-types.md
+++ b/content/code-security/reference/secret-security/secret-types.md
@@ -61,7 +61,7 @@ Organization-level secrets:
### Limitations and restrictions
-For workflows initiated by {% data variables.product.prodname_dependabot %}, the `pull_request_target` event is treated differently to other events. For this event, if the base ref of the pull request was created by {% data variables.product.prodname_dependabot %} (`github.event.pull_request.user.login == 'dependabot[bot]'`):
+For workflows initiated by {% data variables.product.prodname_dependabot %}, the `pull_request_target` event is treated differently to other events. For this event, if the base ref of the pull request was created by {% data variables.product.prodname_dependabot %}:
* The workflow receives a read-only `GITHUB_TOKEN`.
* Secrets are **not** available to the workflow.
diff --git a/content/code-security/reference/supply-chain-security/dependabot-on-actions.md b/content/code-security/reference/supply-chain-security/dependabot-on-actions.md
index 43636ee18740..7c7015968503 100644
--- a/content/code-security/reference/supply-chain-security/dependabot-on-actions.md
+++ b/content/code-security/reference/supply-chain-security/dependabot-on-actions.md
@@ -20,7 +20,7 @@ For workflows initiated by {% data variables.product.prodname_dependabot %} (`gi
* `GITHUB_TOKEN` has read-only permissions by default.
* Secrets are populated from {% data variables.product.prodname_dependabot %} secrets. {% data variables.product.prodname_actions %} secrets are not available.
-For workflows initiated by {% data variables.product.prodname_dependabot %} (`github.actor == 'dependabot[bot]'`) using the `pull_request_target` event, if the base ref of the pull request was created by {% data variables.product.prodname_dependabot %} (`github.event.pull_request.user.login == 'dependabot[bot]'`), the `GITHUB_TOKEN` will be read-only and secrets are not available.
+For workflows initiated by {% data variables.product.prodname_dependabot %} (`github.actor == 'dependabot[bot]'`) using the `pull_request_target` event, if the base ref of the pull request was created by {% data variables.product.prodname_dependabot %}, the `GITHUB_TOKEN` will be read-only and secrets are not available.
These restrictions apply even if the workflow is re-run by a different actor.
diff --git a/content/code-security/tutorials/remediate-leaked-secrets/evaluating-alerts.md b/content/code-security/tutorials/remediate-leaked-secrets/evaluating-alerts.md
index 9bd8613c471a..9d106d94174f 100644
--- a/content/code-security/tutorials/remediate-leaked-secrets/evaluating-alerts.md
+++ b/content/code-security/tutorials/remediate-leaked-secrets/evaluating-alerts.md
@@ -31,7 +31,7 @@ Validity checks help you prioritize alerts by telling you which secrets are `act
By default, {% data variables.product.company_short %} checks the validity of {% data variables.product.company_short %} tokens and displays the validation status of the token in the alert view.
-{% ifversion secret-scanning-validity-check-partner-patterns %}Organizations using {% data variables.product.prodname_team %}, {% data variables.product.prodname_ghe_cloud %} with a license for {% data variables.product.prodname_GH_secret_protection %}, or {% data variables.product.prodname_ghe_server %} with a license for {% data variables.product.prodname_GH_secret_protection %} can also enable validity checks for partner patterns. For more information, see [Checking a secret's validity](/code-security/tutorials/remediate-leaked-secrets/evaluating-alerts#checking-a-secrets-validity).{% endif %}
+{% ifversion secret-scanning-validity-check-partner-patterns %}Organizations using {% data variables.product.prodname_team %}, {% data variables.product.prodname_ghe_cloud %} with a license for {% data variables.product.prodname_GH_secret_protection %}, or {% data variables.product.prodname_ghe_server %} with a license for {% data variables.product.prodname_GH_secret_protection %} can also enable validity checks for partner patterns. {% endif %}
{% data reusables.secret-scanning.validity-check-table %}
diff --git a/content/copilot/concepts/copilot-surfaces/copilot-in-ides.md b/content/copilot/concepts/copilot-surfaces/copilot-in-ides.md
index 7347d7c69608..d287370d7b3c 100644
--- a/content/copilot/concepts/copilot-surfaces/copilot-in-ides.md
+++ b/content/copilot/concepts/copilot-surfaces/copilot-in-ides.md
@@ -1,7 +1,7 @@
---
title: GitHub Copilot in IDEs
shortTitle: Copilot in IDEs
-intro: 'Get contextual AI assistance from {% data variables.product.prodname_copilot %} throughout your IDE workflow, from exploring an approach to writing, reviewing, and improving code.'
+intro: 'Use {% data variables.product.prodname_copilot_short %} in your IDE to get assistance as you code, collaborate with agents, and delegate work locally, in the background, or in the cloud.'
versions:
feature: copilot
defaultTool: vscode
@@ -14,66 +14,43 @@ redirect_from:
- /copilot/concepts/agents/copilot-in-jetbrains
---
-{% data variables.product.prodname_copilot %} integrates with supported IDEs to help you throughout the development process. {% data variables.product.prodname_copilot_short %} can explain concepts, complete code, propose edits, and validate files with agent mode.
+{% data variables.product.prodname_copilot %} integrates with supported IDEs to help you throughout the development process. You can work alongside {% data variables.product.prodname_copilot_short %} in your editor, or delegate tasks to agents and review their results.
The available features and entry points vary by IDE. For a detailed comparison, see [AUTOTITLE](/copilot/reference/copilot-feature-matrix).
-## How {% data variables.product.prodname_copilot_short %} helps in your IDE
+## {% data variables.product.prodname_copilot_short %} in {% data variables.product.prodname_vscode_shortname %}
-You can work with {% data variables.product.prodname_copilot_short %} in three main ways:
+{% data variables.product.prodname_copilot_short %} in {% data variables.product.prodname_vscode_shortname %} brings AI assistance and agent-driven development into the environment where you write, run, debug, and review code.
-* **Code suggestions** help you write and edit code without leaving the editor.
-* **Chat** lets you ask questions, explain or refactor code, generate tests, and explore possible solutions.
-* **Agentic experiences** can plan and complete multi-step tasks by reading files, editing code, and running commands in your local development environment.
+### Benefits of using {% data variables.product.prodname_copilot_short %} in {% data variables.product.prodname_vscode_shortname %}
-These experiences complement each other. For example, you might use chat to understand an unfamiliar codebase, ask an agent to implement a change across several files, and use inline suggestions to refine the result.
+* **Stay in your development environment.** Work with agents alongside your editor, terminal, tests, source control, and debugger, so you can inspect and validate results with the tools you already use.
+* **Choose how much work to delegate.** Move between code suggestions, focused edits, interactive chat, planning, and autonomous agent work as your task evolves.
+* **Work on multiple tasks.** Run isolated agent sessions in parallel and use the Agents window to follow progress across projects.
+* **Use the environment your project needs.** Run agents where your code and tools live, whether on your machine or in a connected remote environment, so they work with the right operating system, runtimes, and dependencies.
+* **Choose your models and tools.** Use models provided with {% data variables.product.prodname_copilot_short %}, bring your own API key, or add models and tools through {% data variables.product.prodname_vscode_shortname %} extensions.
-## Code suggestions
+### What you can do with {% data variables.product.prodname_copilot_short %} in {% data variables.product.prodname_vscode_shortname %}
-{% data variables.product.prodname_copilot_short %} can suggest code directly in the editor as you work. Depending on your IDE, these suggestions can include inline suggestions and {% data variables.copilot.next_edit_suggestions %}.
+Choose the most appropriate workflow for your task, from getting focused assistance in the editor to delegating work to agents.
-### Inline suggestions
+* **Code suggestions:** Get suggestions directly in the editor as you work. See [AUTOTITLE](/copilot/how-tos/get-code-suggestions/get-ide-code-suggestions).
+* **Chat:** Ask questions about your codebase, compare approaches, and plan complex changes using context from your open files and workspace. Use inline chat for quick help within your code. See [AUTOTITLE](/copilot/how-tos/chat-with-copilot/chat-in-ide).
+* **Agent mode:** Ask an agent to find relevant code, edit files, run commands, and validate its work, while you review changes and redirect it as it works. See [AUTOTITLE](/copilot/how-tos/copilot-in-your-ide/use-copilot-agents/use-agent-mode).
+* **Parallel agent sessions:** Start, monitor, and review multiple agent sessions across projects from the Agents window. See [Agents window](https://code.visualstudio.com/docs/agents/run/agents-window) in the {% data variables.product.prodname_vscode_shortname %} documentation.
+* **Cloud agent sessions:** Manage {% data variables.copilot.copilot_cloud_agent %} sessions directly from your IDE. See [AUTOTITLE](/copilot/how-tos/copilot-in-your-ide/use-copilot-agents/use-cloud-agent-in-your-ide).
+* **Models and agent harnesses:** Choose a model based on the speed, reasoning, and cost your task needs. Use models included with {% data variables.product.prodname_copilot_short %}, bring your own key (BYOK), or install an extension that provides more models. You can also choose an agent harness. See [AUTOTITLE](/copilot/concepts/models/bring-your-own-key) and [Agent harnesses](https://code.visualstudio.com/docs/agents/run/agent-harnesses) in the {% data variables.product.prodname_vscode_shortname %} documentation.
+* **Local, remote, and containerized environments:** Run agents on your machine, in a Dev Container (Experimental), or on a remote machine over SSH or a dev tunnel. You can check progress from the Agents window, including in a browser on another device.
+* **Customizations:** Add project-specific context and capabilities with custom instructions, agent skills, hooks, Model Context Protocol (MCP) servers, and extensions.
+* **Terminal:** Run {% data variables.copilot.copilot_cli %} in the integrated terminal for a terminal-first agent experience.
-Inline suggestions appear as dimmed text at your cursor position. {% data variables.product.prodname_copilot_short %} uses the code around your cursor and other available context to predict what you are likely to write next.
+For a full overview, see [Build with AI in VS Code](https://code.visualstudio.com/docs/agents/overview) in the {% data variables.product.prodname_vscode_shortname %} documentation.
-Suggestions can complete part of a line, an entire line, or a larger block of code. You can also write a natural-language comment describing the code you want, then let {% data variables.product.prodname_copilot_short %} suggest an implementation.
+## Using {% data variables.product.prodname_copilot_short %} in other IDEs
-You remain responsible for reviewing and testing suggested code before using it.
+For most supported IDEs, the **{% data variables.product.prodname_copilot %} extension or plugin** is the primary entry point. It connects {% data variables.product.prodname_copilot_short %} to your editor and provides the features supported by that IDE. To install the appropriate integration, see [AUTOTITLE](/copilot/how-tos/set-up/install-copilot-extension).
-### {% data variables.copilot.next_edit_suggestions_caps %}
-
-{% data variables.copilot.next_edit_suggestions_caps %} predict both where you are likely to make your next change and what that change could be. They are based on the edits you are currently making and can help you apply related changes elsewhere in a file.
-
-For example, after you rename a method or change a data structure, {% data variables.product.prodname_copilot_short %} might suggest updating another location affected by that change.
-
-Availability and configuration vary by IDE. For instructions on using inline and next edit suggestions, see [AUTOTITLE](/copilot/how-tos/get-code-suggestions/get-ide-code-suggestions).
-
-{% data reusables.copilot.supported-languages %}
-
-## Chat and agentic experiences
-
-{% data variables.copilot.copilot_chat %} provides a conversational interface in your IDE. Because chat can use context from your project, you can ask questions about selected code, open files, or your wider codebase.
-
-You can use chat to:
-
-* Explain unfamiliar code.
-* Suggest fixes for bugs.
-* Refactor or document code.
-* Generate tests.
-* Compare implementation approaches.
-* Answer questions about programming languages and development tools.
-
-For larger tasks, an agentic experience can break your request into steps and use tools to complete the work. Depending on your IDE and configuration, an agent can inspect your project, edit multiple files, run terminal commands, and respond to errors encountered while working.
-
-Agentic changes are made in your local development environment. Review the proposed changes and the output of any commands before accepting the result.
-
-For instructions on using chat and agents, see [AUTOTITLE](/copilot/how-tos/chat-with-copilot/chat-in-ide).
-
-## Choosing an entry point
-
-For most IDEs, the **{% data variables.product.prodname_copilot %} extension or plugin** is the primary entry point. It connects {% data variables.product.prodname_copilot_short %} directly to your editor and provides the features supported by that IDE. You can install the appropriate integration by following [AUTOTITLE](/copilot/how-tos/set-up/install-copilot-extension).
-
-You can also run **{% data variables.copilot.copilot_cli %}** in an IDE's integrated terminal if the terminal environment meets the requirements for {% data variables.copilot.copilot_cli_short %}. Integration between CLI sessions and the IDE varies.
+You can also run {% data variables.copilot.copilot_cli %} in an IDE's integrated terminal if the terminal environment meets the requirements for {% data variables.copilot.copilot_cli_short %}. Integration between CLI sessions and the IDE varies.
### Entry points in JetBrains IDEs
diff --git a/content/copilot/concepts/copilot-surfaces/github-copilot-app.md b/content/copilot/concepts/copilot-surfaces/github-copilot-app.md
index e2a167ff1612..47d02499bd83 100644
--- a/content/copilot/concepts/copilot-surfaces/github-copilot-app.md
+++ b/content/copilot/concepts/copilot-surfaces/github-copilot-app.md
@@ -2,7 +2,7 @@
title: GitHub Copilot app
shortTitle: GitHub Copilot app
intro: 'The {% data variables.copilot.github_copilot_app %} is a desktop application for agent-driven development that brings parallel workstreams, {% data variables.product.github %} integration, and PR lifecycle management into one place.'
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: concepts
diff --git a/content/copilot/get-started/quickstart-copilot-app.md b/content/copilot/get-started/quickstart-copilot-app.md
index a569edc1a43c..7e48daef839a 100644
--- a/content/copilot/get-started/quickstart-copilot-app.md
+++ b/content/copilot/get-started/quickstart-copilot-app.md
@@ -3,7 +3,7 @@ title: Getting started with the GitHub Copilot app
shortTitle: Copilot app quickstart
allowTitleToDifferFromFilename: true
intro: 'Sign in to the {% data variables.copilot.github_copilot_app %}, connect a repository or local folder, and create your first agent session to make code changes.'
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: get-started
@@ -35,8 +35,9 @@ In this quickstart, you will:
## Installing the {% data variables.copilot.github_copilot_app %}
-1. Visit the [download page for the {% data variables.copilot.github_copilot_app %}](https://github.com/features/ai/github-app).
-1. Download the app for your platform.
+Download and install the {% data variables.copilot.github_copilot_app %} for your operating system. For other operating systems and architectures, see the [download page](https://github.com/features/ai/github-app).
+
+{% data reusables.copilot.github-app-download-button %}
## Opening the {% data variables.copilot.github_copilot_app %} for the first time
diff --git a/content/copilot/how-tos/copilot-in-your-ide/copilot-for-common-tasks/get-ide-code-suggestions.md b/content/copilot/how-tos/copilot-in-your-ide/copilot-for-common-tasks/get-ide-code-suggestions.md
index baed52bd3e1a..4fe46b018c25 100644
--- a/content/copilot/how-tos/copilot-in-your-ide/copilot-for-common-tasks/get-ide-code-suggestions.md
+++ b/content/copilot/how-tos/copilot-in-your-ide/copilot-for-common-tasks/get-ide-code-suggestions.md
@@ -40,6 +40,8 @@ category:
This guide demonstrates how to get coding suggestions from {% data variables.product.prodname_copilot %} in a JetBrains IDE. To see instructions for other popular coding environments, use the tool switcher at the top of the page.
+{% data reusables.copilot.supported-languages %}
+
The examples in this guide use Java, however other languages will work similarly.
For more information, see [AUTOTITLE](/copilot/concepts/copilot-surfaces/copilot-in-ides#code-suggestions).
@@ -221,7 +223,7 @@ To enable comment suggestions, you need to configure the comment style in {% dat
1. Under the **Code Style** heading, click **General**.
1. Under "Comments," select **Xml Doc Comments** from the dropdown.
1. Select **Insert existing comment style at the start of new lines when writing comments** and **Continue single line comments.** Then click **OK**.
-1. In the **Options** tab, in the left-side panel, click **GitHub**.
+1. In the **Options** tab, in the left-side panel, click **GitHub**.
1. Click **Copilot**, then **Editor**.
1. Select **Enable AI generated descriptions for auto-inserted documentation comments in supported languages**.
@@ -231,7 +233,7 @@ To enable comment suggestions, you need to configure the comment style in {% dat
1. In the left-side panel, click **Languages**.
1. Click **C#**, then **More Settings**, then **Advanced**.
1. Under "Comments," select **Generate XML documentation comments for ///**, **Insert // at the start of new lines when writing // comments**, and **Insert \* at the start of new lines when writing /\* \*/ comments.** Then click **OK**.
-1. In the **Options** tab, in the left-side panel, click **GitHub**.
+1. In the **Options** tab, in the left-side panel, click **GitHub**.
1. Click **Copilot**, then **Editor**.
1. Select **Enable AI generated descriptions for auto-inserted documentation comments in supported languages**.
diff --git a/content/copilot/how-tos/github-copilot-app/agent-sessions.md b/content/copilot/how-tos/github-copilot-app/agent-sessions.md
index a70bfa5f7e9b..954674c83b65 100644
--- a/content/copilot/how-tos/github-copilot-app/agent-sessions.md
+++ b/content/copilot/how-tos/github-copilot-app/agent-sessions.md
@@ -3,7 +3,7 @@ title: Working with agent sessions in the GitHub Copilot app
shortTitle: Agent sessions
intro: 'Run multiple isolated agent sessions simultaneously, each with its own branch, and steer them using different session modes, models, and tools.'
allowTitleToDifferFromFilename: true
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: how-tos
diff --git a/content/copilot/how-tos/github-copilot-app/computer-use.md b/content/copilot/how-tos/github-copilot-app/computer-use.md
index 46742baf3425..af7aff5a9519 100644
--- a/content/copilot/how-tos/github-copilot-app/computer-use.md
+++ b/content/copilot/how-tos/github-copilot-app/computer-use.md
@@ -2,7 +2,7 @@
title: Using the GitHub Copilot app to interact with desktop applications
shortTitle: Computer use
intro: 'With computer use, allow {% data variables.product.prodname_copilot_short %} to interact with local desktop applications from the {% data variables.copilot.github_copilot_app %}.'
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: how-tos
diff --git a/content/copilot/how-tos/github-copilot-app/customize-github-copilot-app.md b/content/copilot/how-tos/github-copilot-app/customize-github-copilot-app.md
index 75c0b9b8c4bc..2e7ea8abdac0 100644
--- a/content/copilot/how-tos/github-copilot-app/customize-github-copilot-app.md
+++ b/content/copilot/how-tos/github-copilot-app/customize-github-copilot-app.md
@@ -3,7 +3,7 @@ title: Customizing the GitHub Copilot app
shortTitle: Customize the GitHub Copilot app
intro: 'Customize the {% data variables.copilot.github_copilot_app %} so it works the way you and your team do.'
allowTitleToDifferFromFilename: true
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: how-tos
diff --git a/content/copilot/how-tos/github-copilot-app/managing-issues-and-pull-requests.md b/content/copilot/how-tos/github-copilot-app/managing-issues-and-pull-requests.md
index bffe7ce235e4..2bba376f945f 100644
--- a/content/copilot/how-tos/github-copilot-app/managing-issues-and-pull-requests.md
+++ b/content/copilot/how-tos/github-copilot-app/managing-issues-and-pull-requests.md
@@ -3,7 +3,7 @@ title: Managing issues and pull requests with the GitHub Copilot app
shortTitle: Managing issues and pull requests
allowTitleToDifferFromFilename: true
intro: 'Pick up an issue, direct an agent to implement changes, and land a pull request—all without leaving the {% data variables.copilot.github_copilot_app %}.'
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: how-tos
diff --git a/content/copilot/how-tos/github-copilot-app/open-with-deep-links.md b/content/copilot/how-tos/github-copilot-app/open-with-deep-links.md
index fec1c49e8182..8551b9410812 100644
--- a/content/copilot/how-tos/github-copilot-app/open-with-deep-links.md
+++ b/content/copilot/how-tos/github-copilot-app/open-with-deep-links.md
@@ -3,7 +3,7 @@ title: Using deep links to open the GitHub Copilot app
shortTitle: Open with deep links
allowTitleToDifferFromFilename: true
intro: "Use deep links to launch the {% data variables.copilot.github_copilot_app %} from the terminal, tickets, and internal tools, so people can jump directly into the right repository, pull request, automation, or session."
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: how-tos
diff --git a/content/copilot/how-tos/github-copilot-app/use-byok-models.md b/content/copilot/how-tos/github-copilot-app/use-byok-models.md
index 3051d7b7a568..dabe88e1e7cc 100644
--- a/content/copilot/how-tos/github-copilot-app/use-byok-models.md
+++ b/content/copilot/how-tos/github-copilot-app/use-byok-models.md
@@ -3,7 +3,7 @@ title: Adding LLM models to the GitHub Copilot app
shortTitle: Add LLM models
intro: 'Connect a model from an external provider of your choice by supplying your own API key, then use the model in agent sessions.'
allowTitleToDifferFromFilename: true
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: how-tos
diff --git a/content/copilot/how-tos/github-copilot-app/using-automations.md b/content/copilot/how-tos/github-copilot-app/using-automations.md
index ef0322491c17..c0d675dac917 100644
--- a/content/copilot/how-tos/github-copilot-app/using-automations.md
+++ b/content/copilot/how-tos/github-copilot-app/using-automations.md
@@ -3,7 +3,7 @@ title: Using automations in the GitHub Copilot app
shortTitle: Automations
intro: 'Automate recurring agent tasks so they run on a schedule or on demand, without manual intervention.'
allowTitleToDifferFromFilename: true
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: how-tos
diff --git a/content/copilot/how-tos/github-copilot-app/working-with-canvas-extensions.md b/content/copilot/how-tos/github-copilot-app/working-with-canvas-extensions.md
index 2b357dc1898f..a382d9030757 100644
--- a/content/copilot/how-tos/github-copilot-app/working-with-canvas-extensions.md
+++ b/content/copilot/how-tos/github-copilot-app/working-with-canvas-extensions.md
@@ -3,7 +3,7 @@ title: Working with canvas extensions in the GitHub Copilot app
shortTitle: Canvas extensions
intro: 'Use canvases in the {% data variables.copilot.github_copilot_app %} to build shared, agent-driven artifacts and interfaces for human-agent collaboration.'
allowTitleToDifferFromFilename: true
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: how-tos
diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md
index e8df32d98912..3bd7dc2bcb88 100644
--- a/content/copilot/reference/ai-models/model-hosting.md
+++ b/content/copilot/reference/ai-models/model-hosting.md
@@ -89,7 +89,7 @@ Used for:
{% data variables.product.prodname_copilot %} uses {% data variables.copilot.copilot_gemini %} models hosted on Google Cloud Platform (GCP). When using {% data variables.copilot.copilot_gemini %} models, prompts and metadata are sent to GCP, which makes the [following data commitment](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance): _{% data variables.copilot.copilot_gemini %} doesn't use your prompts, or its responses, as data to train its models._
-To provide better service quality and reduce latency, {% data variables.product.github %} uses Google's Durable Caching. Cached data is encrypted and held for up to 24 hours. Cached data is never used to train or fine-tune models.
+To provide better service quality and reduce latency, {% data variables.product.github %} uses [Google's Durable Caching](https://docs.cloud.google.com/gemini-enterprise-agent-platform/resources/zero-data-retention#durable-data-caching). Cached data is encrypted and held for up to 24 hours. Cached data is never used to train or fine-tune models.
When using {% data variables.copilot.copilot_gemini %} models, input prompts and output completions continue to run through {% data variables.product.prodname_copilot %}'s content filters for public code matching, when applied, along with those for harmful or offensive content.
diff --git a/content/copilot/reference/github-copilot-app-reference/built-in-skills.md b/content/copilot/reference/github-copilot-app-reference/built-in-skills.md
index fd909c46df66..c6577c60ca1d 100644
--- a/content/copilot/reference/github-copilot-app-reference/built-in-skills.md
+++ b/content/copilot/reference/github-copilot-app-reference/built-in-skills.md
@@ -3,7 +3,7 @@ title: Built-in skills for the GitHub Copilot app
shortTitle: Built-in skills
allowTitleToDifferFromFilename: true
intro: 'Use this reference to find built-in agent skills available in the {% data variables.copilot.github_copilot_app %}.'
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: reference
diff --git a/content/copilot/reference/github-copilot-app-reference/repository-configuration.md b/content/copilot/reference/github-copilot-app-reference/repository-configuration.md
index 23c5ff995d83..ff0a43c3c6ce 100644
--- a/content/copilot/reference/github-copilot-app-reference/repository-configuration.md
+++ b/content/copilot/reference/github-copilot-app-reference/repository-configuration.md
@@ -3,7 +3,7 @@ title: Repository configuration for the GitHub Copilot app
shortTitle: Repository configuration
allowTitleToDifferFromFilename: true
intro: 'Define repository-specific instructions, scripts, and automation behavior for the {% data variables.copilot.github_copilot_app %}.'
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: reference
diff --git a/content/copilot/reference/github-copilot-app-reference/slash-commands.md b/content/copilot/reference/github-copilot-app-reference/slash-commands.md
index 68215ea1bec1..f30067d240b1 100644
--- a/content/copilot/reference/github-copilot-app-reference/slash-commands.md
+++ b/content/copilot/reference/github-copilot-app-reference/slash-commands.md
@@ -3,7 +3,7 @@ title: Slash commands for the GitHub Copilot app
shortTitle: Slash commands
allowTitleToDifferFromFilename: true
intro: 'Use slash commands in the {% data variables.copilot.github_copilot_app %} to switch modes, run common workflows, and manage sessions without writing a long prompt.'
-product: '{% data reusables.gated-features.github-app %} Download the {% data variables.copilot.github_copilot_app %} {% octicon "link-external" height:16 %}'
+product: '{% data reusables.gated-features.github-app %} {% data reusables.copilot.github-app-download-button %}'
versions:
feature: copilot
contentType: reference
diff --git a/data/reusables/copilot/github-app-download-button.md b/data/reusables/copilot/github-app-download-button.md
new file mode 100644
index 000000000000..bf1f06c64a3c
--- /dev/null
+++ b/data/reusables/copilot/github-app-download-button.md
@@ -0,0 +1 @@
+{% octicon "desktop-download" aria-hidden="true" %} Download the {% data variables.copilot.github_copilot_app %}
diff --git a/data/ui.yml b/data/ui.yml
index f71b85944b51..a88b9607bc29 100644
--- a/data/ui.yml
+++ b/data/ui.yml
@@ -132,6 +132,7 @@ pages:
more_markdown_options: More Markdown options
opens_in_new_tab: (opens in new tab)
copied: Copied!
+ download_for: 'Download for {os}'
support:
still_need_help: Still need help?
diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml
index f71b85944b51..a88b9607bc29 100644
--- a/src/fixtures/fixtures/data/ui.yml
+++ b/src/fixtures/fixtures/data/ui.yml
@@ -132,6 +132,7 @@ pages:
more_markdown_options: More Markdown options
opens_in_new_tab: (opens in new tab)
copied: Copied!
+ download_for: 'Download for {os}'
support:
still_need_help: Still need help?
diff --git a/src/frame/components/ui/MarkdownContent/markdownComponents.tsx b/src/frame/components/ui/MarkdownContent/markdownComponents.tsx
index 1887f27ae559..c7feef0a0a07 100644
--- a/src/frame/components/ui/MarkdownContent/markdownComponents.tsx
+++ b/src/frame/components/ui/MarkdownContent/markdownComponents.tsx
@@ -5,16 +5,25 @@ import { Image } from '@primer/react-brand'
import { CopyButton } from '@/frame/components/CopyButton'
import { CodeTabsGroup } from '@/frame/components/CodeTabsGroup'
import { ToggleableContent } from '@/tools/components/ToggleableContent'
+import { OsDownloadLink } from '@/tools/components/OsDownloadLink'
import { isToggleClass } from '@/tools/components/SelectionContext'
const EMOJI_SRC = 'https://github.githubassets.com/images/icons/emoji'
-// Shared HTML AST paths map picker and copy-button markup to React components
+// Shared HTML AST paths map picker, copy-button, and OS download markup to React components
// instead of post-hydration DOM mutation. Article bodies and RenderedHTML
// fragments use this map, so SSR and hydration stay in sync.
// Class checks run first; unrecognized classes fall through to plain elements,
// so GraphQL and REST descriptions without pickers stay plain.
export const markdownComponents = {
+ a(props: ComponentProps<'a'>) {
+ const classes = String(props.className || '').split(/\s+/)
+ if (classes.includes('js-os-download')) {
+ return
+ }
+ const { children, ...rest } = props
+ return {children}
+ },
button(props: ComponentProps<'button'>) {
const classes = String(props.className || '').split(/\s+/)
if (classes.includes('js-btn-copy')) {
diff --git a/src/tools/README.md b/src/tools/README.md
index 85725032f74b..a918bc0e5480 100644
--- a/src/tools/README.md
+++ b/src/tools/README.md
@@ -16,6 +16,7 @@ The primary goal is to provide a personalized reading experience by:
The available options are defined in static configuration files:
- **`all-tools.ts`**: Maps internal IDs (e.g., `cli`, `webui`) to display names. The order of keys in this file determines the display order in the UI, often sorted by usage analytics.
- **`all-platforms.ts`**: Defines operating systems (Mac, Windows, Linux).
+- **`os-download.ts`**: Detects the reader's OS and CPU architecture for OS-aware download buttons. Uses User-Agent Client Hints when available and falls back to the user agent string.
### Components (`src/tools/components`)
@@ -23,6 +24,22 @@ The UI is built with React components that interact with the global application
- **`ToolPicker.tsx`**: The dropdown or tab interface for selecting a tool.
- **`PlatformPicker.tsx`**: The interface for selecting an operating system.
- **`InArticlePicker.tsx`**: A specialized picker that appears within the article body (often used for "Prerequisites" sections).
+- **`OsDownloadLink.tsx`**: Renders links with the `js-os-download` class. After hydration, it swaps `href` to the matching `data-os-download-` attribute and the label to "Download for {os}". If detection fails, the link keeps its original `href`. See `data/reusables/copilot/github-app-download-button.md` for an example.
+
+### OS-aware download links
+
+Add the `js-os-download` class and one or more of these data attributes to a link in Markdown or a reusable:
+
+| Attribute | Used for |
+| --- | --- |
+| `data-os-download-mac` | macOS on Apple silicon, and macOS when the architecture is unknown |
+| `data-os-download-mac-intel` | macOS on Intel |
+| `data-os-download-windows` | Windows on x64, and Windows when the architecture is unknown |
+| `data-os-download-windows-arm` | Windows on Arm |
+| `data-os-download-linux` | Linux on x64, and Linux when the architecture is unknown |
+| `data-os-download-linux-arm` | Linux on Arm |
+
+Wrap the label text in `` to have it replaced with "Download for {os}". iOS, Android, ChromeOS, and missing attributes keep the original `href`.
### State Management
diff --git a/src/tools/components/OsDownloadLink.tsx b/src/tools/components/OsDownloadLink.tsx
new file mode 100644
index 000000000000..45cf5ba15657
--- /dev/null
+++ b/src/tools/components/OsDownloadLink.tsx
@@ -0,0 +1,59 @@
+import { Children, cloneElement, isValidElement, useEffect, useState } from 'react'
+import type { ComponentPropsWithoutRef, ReactNode } from 'react'
+
+import { useTranslation } from '@/languages/components/useTranslation'
+import {
+ DOWNLOAD_ATTRIBUTE_PREFIX,
+ detectDownloadKey,
+ osNameForKey,
+ type DownloadKey,
+} from '@/tools/lib/os-download'
+
+export const OS_DOWNLOAD_LABEL_CLASS = 'js-os-download-label'
+
+type Props = ComponentPropsWithoutRef<'a'> & { [attribute: `data-${string}`]: string | undefined }
+
+// Content marks a link with a.js-os-download and lists installers in
+// data-os-download- attributes. Pages are CDN-cached, so the server render
+// keeps the fallback href, and the installer swaps in after hydration.
+export function OsDownloadLink({ children, href, target, ...props }: Props) {
+ const { t } = useTranslation('pages')
+ const [key, setKey] = useState(null)
+
+ useEffect(() => {
+ let cancelled = false
+ async function detect() {
+ const detected = await detectDownloadKey(navigator)
+ if (!cancelled) setKey(detected)
+ }
+ detect()
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ const downloadHref = key ? props[`${DOWNLOAD_ATTRIBUTE_PREFIX}${key}`] : undefined
+ if (!key || !downloadHref) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ // Installers download in place, so drop target="_blank" to avoid an empty tab.
+ const label = t('download_for').replace('{os}', osNameForKey(key))
+ return (
+
+ {replaceDownloadLabel(children, label)}
+
+ )
+}
+
+export function replaceDownloadLabel(children: ReactNode, label: string): ReactNode {
+ return Children.map(children, (child) => {
+ if (!isValidElement<{ className?: string }>(child)) return child
+ const classes = String(child.props.className || '').split(/\s+/)
+ return classes.includes(OS_DOWNLOAD_LABEL_CLASS) ? cloneElement(child, undefined, label) : child
+ })
+}
diff --git a/src/tools/lib/os-download.ts b/src/tools/lib/os-download.ts
new file mode 100644
index 000000000000..adabc56f25cc
--- /dev/null
+++ b/src/tools/lib/os-download.ts
@@ -0,0 +1,73 @@
+import { parseUserAgent } from '@/events/components/user-agent'
+
+export type DownloadKey = 'mac' | 'mac-intel' | 'windows' | 'windows-arm' | 'linux' | 'linux-arm'
+export type Arch = 'arm' | 'x86' | 'unknown'
+
+export const DOWNLOAD_ATTRIBUTE_PREFIX = 'data-os-download-'
+
+const OS_NAMES: Record = {
+ mac: 'macOS',
+ windows: 'Windows',
+ linux: 'Linux',
+}
+
+type DetectInput = {
+ os: string
+ arch: Arch
+ // iPadOS Safari reports a Mac user agent, but has touch support.
+ isTouchMac?: boolean
+}
+
+// Unknown architecture follows github.com/features/ai/github-app:
+// macOS defaults to Apple silicon, and Windows and Linux default to x64.
+export function pickDownloadKey({ os, arch, isTouchMac }: DetectInput): DownloadKey | null {
+ if (os === 'mac') {
+ if (isTouchMac) return null
+ return arch === 'x86' ? 'mac-intel' : 'mac'
+ }
+ if (os === 'windows') return arch === 'arm' ? 'windows-arm' : 'windows'
+ if (os === 'linux') return arch === 'arm' ? 'linux-arm' : 'linux'
+ return null
+}
+
+export function osNameForKey(key: DownloadKey): string {
+ return OS_NAMES[key.split('-')[0]]
+}
+
+// Firefox on Linux reports the real architecture in the user agent.
+// Chromium freezes it to x86_64, so prefer client hints when they exist.
+export function archFromUserAgent(ua: string): Arch {
+ const lower = ua.toLowerCase()
+ if (/\b(aarch64|arm64|armv\d)/.test(lower)) return 'arm'
+ if (/\bx86_64\b/.test(lower)) return 'x86'
+ return 'unknown'
+}
+
+type UADataLike = {
+ getHighEntropyValues?: (hints: string[]) => Promise<{ architecture?: string }>
+}
+
+export type NavigatorLike = {
+ userAgent: string
+ maxTouchPoints?: number
+ userAgentData?: UADataLike
+}
+
+async function archFromClientHints(nav: NavigatorLike): Promise {
+ try {
+ const values = await nav.userAgentData?.getHighEntropyValues?.(['architecture'])
+ if (values?.architecture === 'arm') return 'arm'
+ if (values?.architecture === 'x86') return 'x86'
+ } catch {
+ // Browsers can reject high entropy hints; fall back to the user agent.
+ }
+ return 'unknown'
+}
+
+export async function detectDownloadKey(nav: NavigatorLike): Promise {
+ const { os } = parseUserAgent(nav.userAgent)
+ let arch = await archFromClientHints(nav)
+ // Mac and Windows user agents freeze the architecture, so only trust it on Linux.
+ if (arch === 'unknown' && os === 'linux') arch = archFromUserAgent(nav.userAgent)
+ return pickDownloadKey({ os, arch, isTouchMac: os === 'mac' && (nav.maxTouchPoints ?? 0) > 1 })
+}
diff --git a/src/tools/tests/os-download.ts b/src/tools/tests/os-download.ts
new file mode 100644
index 000000000000..67c451518249
--- /dev/null
+++ b/src/tools/tests/os-download.ts
@@ -0,0 +1,144 @@
+import { createElement } from 'react'
+import { renderToStaticMarkup } from 'react-dom/server'
+import { afterEach, describe, expect, test, vi } from 'vitest'
+
+import type { Components } from 'hast-util-to-jsx-runtime'
+
+import { renderHTMLString } from '@/frame/components/ui/RenderedHTML/render-html-string'
+import { OsDownloadLink, replaceDownloadLabel } from '@/tools/components/OsDownloadLink'
+import {
+ archFromUserAgent,
+ detectDownloadKey,
+ pickDownloadKey,
+ type NavigatorLike,
+} from '@/tools/lib/os-download'
+
+// Mock the translation hook so server rendering does not need MainContext.
+vi.mock('@/languages/components/useTranslation', () => ({
+ useTranslation: () => ({
+ t: (key: string) => (key === 'download_for' ? 'Download for {os}' : key),
+ }),
+}))
+
+const UA = {
+ macChrome:
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
+ macSafari:
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15',
+ windowsEdge:
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0',
+ linuxFirefoxArm: 'Mozilla/5.0 (X11; Linux aarch64; rv:143.0) Gecko/20100101 Firefox/143.0',
+ linuxFirefoxX64: 'Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0',
+ linuxChrome:
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
+ android:
+ 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36',
+ iphone:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
+ chromeOS:
+ 'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
+}
+
+function nav(userAgent: string, extra: Partial = {}): NavigatorLike {
+ return { userAgent, ...extra }
+}
+
+function withArch(architecture: string): NavigatorLike['userAgentData'] {
+ return { getHighEntropyValues: async () => ({ architecture }) }
+}
+
+describe('pickDownloadKey', () => {
+ test.each([
+ [{ os: 'mac', arch: 'unknown' }, 'mac'],
+ [{ os: 'mac', arch: 'arm' }, 'mac'],
+ [{ os: 'mac', arch: 'x86' }, 'mac-intel'],
+ [{ os: 'mac', arch: 'arm', isTouchMac: true }, null],
+ [{ os: 'windows', arch: 'unknown' }, 'windows'],
+ [{ os: 'windows', arch: 'arm' }, 'windows-arm'],
+ [{ os: 'linux', arch: 'x86' }, 'linux'],
+ [{ os: 'linux', arch: 'arm' }, 'linux-arm'],
+ [{ os: 'ios', arch: 'arm' }, null],
+ [{ os: 'android', arch: 'arm' }, null],
+ [{ os: 'cros', arch: 'x86' }, null],
+ [{ os: 'other', arch: 'unknown' }, null],
+ ] as const)('%o -> %s', (input, expected) => {
+ expect(pickDownloadKey(input)).toBe(expected)
+ })
+})
+
+describe('archFromUserAgent', () => {
+ test('reads Linux architecture tokens', () => {
+ expect(archFromUserAgent(UA.linuxFirefoxArm)).toBe('arm')
+ expect(archFromUserAgent(UA.linuxFirefoxX64)).toBe('x86')
+ expect(archFromUserAgent('Mozilla/5.0 (X11; Linux i686)')).toBe('unknown')
+ })
+})
+
+describe('detectDownloadKey', () => {
+ test('uses client hints for architecture when available', async () => {
+ expect(await detectDownloadKey(nav(UA.macChrome, { userAgentData: withArch('x86') }))).toBe(
+ 'mac-intel',
+ )
+ expect(await detectDownloadKey(nav(UA.macChrome, { userAgentData: withArch('arm') }))).toBe(
+ 'mac',
+ )
+ expect(await detectDownloadKey(nav(UA.windowsEdge, { userAgentData: withArch('arm') }))).toBe(
+ 'windows-arm',
+ )
+ // Chromium freezes Linux user agents to x86_64, so client hints win.
+ expect(await detectDownloadKey(nav(UA.linuxChrome, { userAgentData: withArch('arm') }))).toBe(
+ 'linux-arm',
+ )
+ })
+
+ test('falls back to defaults without client hints', async () => {
+ expect(await detectDownloadKey(nav(UA.macSafari))).toBe('mac')
+ expect(await detectDownloadKey(nav(UA.windowsEdge))).toBe('windows')
+ expect(await detectDownloadKey(nav(UA.linuxFirefoxX64))).toBe('linux')
+ expect(await detectDownloadKey(nav(UA.linuxFirefoxArm))).toBe('linux-arm')
+ })
+
+ test('falls back when client hints reject', async () => {
+ const userAgentData = {
+ getHighEntropyValues: () => Promise.reject(new Error('blocked')),
+ }
+ expect(await detectDownloadKey(nav(UA.windowsEdge, { userAgentData }))).toBe('windows')
+ })
+
+ test('skips iPad, phones, and ChromeOS', async () => {
+ expect(await detectDownloadKey(nav(UA.macSafari, { maxTouchPoints: 5 }))).toBeNull()
+ expect(await detectDownloadKey(nav(UA.iphone))).toBeNull()
+ expect(await detectDownloadKey(nav(UA.android))).toBeNull()
+ expect(await detectDownloadKey(nav(UA.chromeOS))).toBeNull()
+ })
+})
+
+describe('OsDownloadLink rendering', () => {
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ // markdownComponents pulls in Primer CSS that Vitest cannot load, so map a directly.
+ test('server render keeps the fallback link unchanged', () => {
+ const html =
+ 'Download app'
+ const components = { a: OsDownloadLink } as unknown as Partial
+ const errors = vi.spyOn(console, 'error').mockImplementation(() => {})
+ expect(renderToStaticMarkup(renderHTMLString(html, components))).toBe(html)
+ expect(errors).not.toHaveBeenCalled()
+ })
+
+ test('replaceDownloadLabel swaps only the label span', () => {
+ const children = [
+ createElement('svg', { key: 'icon', 'aria-hidden': 'true' }),
+ ' ',
+ createElement('span', { key: 'label', className: 'js-os-download-label' }, 'Download app'),
+ ]
+ const out = renderToStaticMarkup(
+ createElement('a', null, replaceDownloadLabel(children, 'Download for macOS')),
+ )
+ expect(out).toBe(
+ 'Download for macOS',
+ )
+ })
+})