feat(website): make Copilot app deep link the default plugin install

Plugin detail pages exposed only a copyable CLI command. They now lead
with a ghapp://plugins/install deep link in the same split-button
ActionMenu the other detail pages use, keeping the CLI command available
as a Copy action in the menu.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 80686fef-efe3-4cdd-8cd6-bfa61a5d0af6
This commit is contained in:
Aaron Powell
2026-08-17 14:45:42 +10:00
parent 8c99585181
commit 82615220c4
2 changed files with 36 additions and 17 deletions
+28 -17
View File
@@ -1,7 +1,8 @@
import { CheckIcon, CopyIcon, MarkGithubIcon } from "@primer/octicons-react"; import { MarkGithubIcon, PlusIcon } from "@primer/octicons-react";
import React from "react"; import React from "react";
import { import {
ActionMenu,
Box, Box,
Button, Button,
Card, Card,
@@ -19,7 +20,6 @@ import { DetailChassis, type DetailSibling } from "./DetailChassis";
import { ResourceMeta } from "./ResourceMeta"; import { ResourceMeta } from "./ResourceMeta";
import { pageHref } from "./pageHref"; import { pageHref } from "./pageHref";
import type { SearchItem } from "./searchIndex"; import type { SearchItem } from "./searchIndex";
import installStyles from "./styles/github-copilot-app.module.css";
import gridStyles from "./styles/plugins.module.css"; import gridStyles from "./styles/plugins.module.css";
import styles from "./styles/dotnet-upgrade.module.css"; import styles from "./styles/dotnet-upgrade.module.css";
@@ -57,6 +57,8 @@ export type PluginDetailProps = {
githubUrl: string; githubUrl: string;
/** Copilot CLI command that installs this plugin. */ /** Copilot CLI command that installs this plugin. */
installCommand: string; installCommand: string;
/** `ghapp://` deep link that installs this plugin into the Copilot app. */
appInstallUrl: string;
lastUpdated?: string | null; lastUpdated?: string | null;
previous?: DetailSibling; previous?: DetailSibling;
next?: DetailSibling; next?: DetailSibling;
@@ -93,14 +95,16 @@ function itemTitle(item: PluginIncludedItem): string {
* *
* Differs from the other resource detail pages in three ways: the plugin's * Differs from the other resource detail pages in three ways: the plugin's
* bundled contents are rendered as a card grid linking to each item's own * bundled contents are rendered as a card grid linking to each item's own
* detail page, installation is a copyable Copilot CLI command rather than a * detail page, installation deep-links into the Copilot app (with the CLI
* VS Code deep link, and externally-hosted plugins carry provenance badges. * command offered as a secondary path), and externally-hosted plugins carry
* provenance badges.
*/ */
export function PluginDetail({ export function PluginDetail({
item, item,
markdownHtml, markdownHtml,
githubUrl, githubUrl,
installCommand, installCommand,
appInstallUrl,
lastUpdated, lastUpdated,
previous, previous,
next, next,
@@ -132,22 +136,29 @@ export function PluginDetail({
const includedItems = item.items ?? []; const includedItems = item.items ?? [];
const hasContents = includedItems.length > 0; const hasContents = includedItems.length > 0;
// Installing straight into the Copilot app is the primary path, so it is the
// split button's default action. The CLI command stays available behind the
// menu for people working in a terminal or on a machine without the app.
const install = ( const install = (
<> <>
<div className={installStyles.installBar}> <ActionMenu mode="split-button" menuAlignment="start">
<code className={installStyles.installCommand} tabIndex={0}> <ActionMenu.Button
{installCommand} as="a"
</code> href={appInstallUrl}
<button variant="primary"
type="button" leadingVisual={PlusIcon}
className={installStyles.installCopy}
onClick={handleCopy}
aria-label={copied ? "Copied to clipboard" : "Copy install command"}
> >
{copied ? <CheckIcon size={16} /> : <CopyIcon size={16} />} Install
<span>{copied ? "Copied" : "Copy"}</span> </ActionMenu.Button>
</button> <ActionMenu.Overlay aria-label={`Install the ${item.name} plugin`}>
</div> <ActionMenu.Item as="a" href={appInstallUrl}>
Install in Copilot app
</ActionMenu.Item>
<ActionMenu.Item onClick={handleCopy}>
{copied ? "Copied CLI command" : "Copy CLI install command"}
</ActionMenu.Item>
</ActionMenu.Overlay>
</ActionMenu>
<Button <Button
as="a" as="a"
href={githubUrl} href={githubUrl}
+8
View File
@@ -69,6 +69,13 @@ const githubUrl = item.external
const installCommand = `copilot plugin install ${item.id}@awesome-copilot`; const installCommand = `copilot plugin install ${item.id}@awesome-copilot`;
// Deep link handled by the Copilot app's `ghapp://` protocol handler. The
// marketplace-qualified source is encoded because the `@` separator is not
// safe unescaped in a query value.
const appInstallUrl = `ghapp://plugins/install?source=${encodeURIComponent(
`${item.id}@awesome-copilot`,
)}`;
const lastUpdated = formatLastUpdated(item.lastUpdated); const lastUpdated = formatLastUpdated(item.lastUpdated);
const base = import.meta.env.BASE_URL ?? "/"; const base = import.meta.env.BASE_URL ?? "/";
@@ -92,6 +99,7 @@ const siblingOf = (sibling?: PluginItem) =>
markdownHtml={html} markdownHtml={html}
githubUrl={githubUrl} githubUrl={githubUrl}
installCommand={installCommand} installCommand={installCommand}
appInstallUrl={appInstallUrl}
lastUpdated={lastUpdated} lastUpdated={lastUpdated}
previous={siblingOf(previous)} previous={siblingOf(previous)}
next={siblingOf(next)} next={siblingOf(next)}