Compare commits

..

1 Commits

Author SHA1 Message Date
Max Schmitt
4cebc50b19 devops: fix publishing 2025-09-08 16:06:23 +02:00
23 changed files with 244 additions and 1710 deletions

47
.github/workflows/publish-canary.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Publish Canary
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
jobs:
publish-canary:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Needed for npm provenance
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: https://registry.npmjs.org/
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Get current version
id: version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Set canary version
id: canary-version
run: echo "version=${{ steps.version.outputs.version }}-alpha-${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
- name: Update package.json version
run: |
npm version ${{ steps.canary-version.outputs.version }} --no-git-tag-version
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run lint
- run: npm run ctest
- name: Publish to npm with next tag
run: npm publish --tag next --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Reset package.json version
run: git checkout -- package.json

View File

@@ -1,82 +1,35 @@
name: Publish
on:
workflow_dispatch:
schedule:
- cron: '0 8 * * *'
release:
types: [published]
jobs:
publish-canary-npm:
if: github.event.schedule || github.event_name == 'workflow_dispatch'
publish-npm:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for OIDC npm publishing
id-token: write # Needed for npm provenance
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 18
registry-url: https://registry.npmjs.org/
# Ensure npm 11.5.1 or later is installed (for OIDC npm publishing)
- name: Update npm
run: npm install -g npm@latest
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Get current version
id: version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Set canary version
id: canary-version
run: echo "version=${{ steps.version.outputs.version }}-alpha-${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
- name: Update package.json version
run: |
npm version ${{ steps.canary-version.outputs.version }} --no-git-tag-version
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run lint
- run: npm run ctest
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to npm with next tag
run: npm publish --tag next
publish-release-npm:
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for OIDC npm publishing
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 20
registry-url: https://registry.npmjs.org/
# Ensure npm 11.5.1 or later is installed (for OIDC npm publishing)
- name: Update npm
run: npm install -g npm@latest
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run lint
- run: npm run ctest
- run: npm publish
publish-release-docker:
if: github.event_name == 'release'
publish-docker:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Needed for OIDC login to Azure
environment: allow-publishing-docker-to-acr
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up QEMU # Needed for multi-platform builds (e.g., arm64 on amd64 runner)
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx # Needed for multi-platform builds
@@ -115,14 +68,13 @@ jobs:
attach_eol_manifest $tag
done
package-release-extension:
if: github.event_name == 'release'
package-extension:
runs-on: ubuntu-latest
permissions:
contents: write # Needed to upload release assets
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'

View File

@@ -51,7 +51,6 @@ FROM base
ARG PLAYWRIGHT_BROWSERS_PATH
ARG USERNAME=node
ENV NODE_ENV=production
ENV PLAYWRIGHT_MCP_OUTPUT_DIR=/tmp/playwright-output
# Set the correct ownership for the runtime user on production `node_modules`
RUN chown -R ${USERNAME}:${USERNAME} node_modules

207
README.md
View File

@@ -76,7 +76,7 @@ For more information, see the [Codex MCP documentation](https://github.com/opena
#### Click the button to install:
[<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install in Cursor">](https://cursor.com/en/install-mcp?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
[<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install in Cursor">](cursor://anysphere.cursor-deeplink/mcp/install?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
#### Or install manually:
@@ -180,90 +180,65 @@ Playwright MCP server supports following arguments. They can be provided in the
```
> npx @playwright/mcp@latest --help
--allowed-hosts <hosts...> comma-separated list of hosts this
server is allowed to serve from.
Defaults to the host the server is bound
to. Pass '*' to disable the host check.
--allowed-origins <origins> semicolon-separated list of origins to
allow the browser to request. Default is
to allow all.
--blocked-origins <origins> semicolon-separated list of origins to
block the browser from requesting.
Blocklist is evaluated before allowlist.
If used without the allowlist, requests
not matching the blocklist are still
allowed.
--block-service-workers block service workers
--browser <browser> browser or chrome channel to use,
possible values: chrome, firefox,
webkit, msedge.
--caps <caps> comma-separated list of additional
capabilities to enable, possible values:
vision, pdf.
--cdp-endpoint <endpoint> CDP endpoint to connect to.
--cdp-header <headers...> CDP headers to send with the connect
request, multiple can be specified.
--config <path> path to the configuration file.
--device <device> device to emulate, for example: "iPhone
15"
--executable-path <path> path to the browser executable.
--extension Connect to a running browser instance
(Edge/Chrome only). Requires the
"Playwright MCP Bridge" browser
extension to be installed.
--grant-permissions <permissions...> List of permissions to grant to the
browser context, for example
"geolocation", "clipboard-read",
"clipboard-write".
--headless run browser in headless mode, headed by
default
--host <host> host to bind server to. Default is
localhost. Use 0.0.0.0 to bind to all
interfaces.
--ignore-https-errors ignore https errors
--init-script <path...> path to JavaScript file to add as an
initialization script. The script will
be evaluated in every page before any of
the page's scripts. Can be specified
multiple times.
--isolated keep the browser profile in memory, do
not save it to disk.
--image-responses <mode> whether to send image responses to the
client. Can be "allow" or "omit",
Defaults to "allow".
--no-sandbox disable the sandbox for all process
types that are normally sandboxed.
--output-dir <path> path to the directory for output files.
--port <port> port to listen on for SSE transport.
--proxy-bypass <bypass> comma-separated domains to bypass proxy,
for example
".com,chromium.org,.domain.com"
--proxy-server <proxy> specify proxy server, for example
"http://myproxy:3128" or
"socks5://myproxy:8080"
--save-session Whether to save the Playwright MCP
session into the output directory.
--save-trace Whether to save the Playwright Trace of
the session into the output directory.
--save-video <size> Whether to save the video of the session
into the output directory. For example
"--save-video=800x600"
--secrets <path> path to a file containing secrets in the
dotenv format
--shared-browser-context reuse the same browser context between
all connected HTTP clients.
--storage-state <path> path to the storage state file for
isolated sessions.
--timeout-action <timeout> specify action timeout in milliseconds,
defaults to 5000ms
--timeout-navigation <timeout> specify navigation timeout in
milliseconds, defaults to 60000ms
--user-agent <ua string> specify user agent string
--user-data-dir <path> path to the user data directory. If not
specified, a temporary directory will be
created.
--viewport-size <size> specify browser viewport size in pixels,
for example "1280x720"
--allowed-origins <origins> semicolon-separated list of origins to allow
the browser to request. Default is to allow
all.
--blocked-origins <origins> semicolon-separated list of origins to block
the browser from requesting. Blocklist is
evaluated before allowlist. If used without
the allowlist, requests not matching the
blocklist are still allowed.
--block-service-workers block service workers
--browser <browser> browser or chrome channel to use, possible
values: chrome, firefox, webkit, msedge.
--caps <caps> comma-separated list of additional
capabilities to enable, possible values:
vision, pdf.
--cdp-endpoint <endpoint> CDP endpoint to connect to.
--cdp-header <headers...> CDP headers to send with the connect request,
multiple can be specified.
--config <path> path to the configuration file.
--device <device> device to emulate, for example: "iPhone 15"
--executable-path <path> path to the browser executable.
--extension Connect to a running browser instance
(Edge/Chrome only). Requires the "Playwright
MCP Bridge" browser extension to be installed.
--headless run browser in headless mode, headed by
default
--host <host> host to bind server to. Default is localhost.
Use 0.0.0.0 to bind to all interfaces.
--ignore-https-errors ignore https errors
--isolated keep the browser profile in memory, do not
save it to disk.
--image-responses <mode> whether to send image responses to the client.
Can be "allow" or "omit", Defaults to "allow".
--no-sandbox disable the sandbox for all process types that
are normally sandboxed.
--output-dir <path> path to the directory for output files.
--port <port> port to listen on for SSE transport.
--proxy-bypass <bypass> comma-separated domains to bypass proxy, for
example ".com,chromium.org,.domain.com"
--proxy-server <proxy> specify proxy server, for example
"http://myproxy:3128" or
"socks5://myproxy:8080"
--save-session Whether to save the Playwright MCP session
into the output directory.
--save-trace Whether to save the Playwright Trace of the
session into the output directory.
--secrets <path> path to a file containing secrets in the
dotenv format
--storage-state <path> path to the storage state file for isolated
sessions.
--timeout-action <timeout> specify action timeout in milliseconds,
defaults to 5000ms
--timeout-navigation <timeout> specify navigation timeout in milliseconds,
defaults to 60000ms
--user-agent <ua string> specify user agent string
--user-data-dir <path> path to the user data directory. If not
specified, a temporary directory will be
created.
--viewport-size <size> specify browser viewport size in pixels, for
example "1280, 720"
```
<!--- End of options generated section -->
@@ -434,19 +409,6 @@ And then in MCP client config, set the `url` to the HTTP endpoint:
}
```
Or If you prefer to run the container as a long-lived service instead of letting the MCP client spawn it, use:
```
docker run -d -i --rm --init --pull=always \
--entrypoint node \
--name playwright \
-p 8931:8931 \
mcr.microsoft.com/playwright/mcp \
cli.js --headless --browser chromium --no-sandbox --port 8931
```
The server will listen on host port **8931** and can be reached by any MCP client.
You can build the Docker image yourself.
```
@@ -469,7 +431,7 @@ http.createServer(async (req, res) => {
// Creates a headless Playwright MCP server with SSE transport
const connection = await createConnection({ browser: { launchOptions: { headless: true } } });
const transport = new SSEServerTransport('/messages', res);
await connection.connect(transport);
await connection.sever.connect(transport);
// ...
});
@@ -509,8 +471,7 @@ http.createServer(async (req, res) => {
- **browser_console_messages**
- Title: Get console messages
- Description: Returns all console messages
- Parameters:
- `onlyErrors` (boolean, optional): Only return error messages
- Parameters: None
- Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js -->
@@ -542,7 +503,7 @@ http.createServer(async (req, res) => {
- Title: Upload files
- Description: Upload one or multiple files
- Parameters:
- `paths` (array, optional): The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
- Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js -->
@@ -761,6 +722,48 @@ http.createServer(async (req, res) => {
<details>
<summary><b>Verify (opt-in via --caps=verify)</b></summary>
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_verify_element_visible**
- Title: Verify element visible
- Description: Verify element is visible on the page
- Parameters:
- `role` (string): ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`
- `accessibleName` (string): ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`
- Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_verify_list_visible**
- Title: Verify list visible
- Description: Verify list is visible on the page
- Parameters:
- `element` (string): Human-readable list description
- `ref` (string): Exact target element reference that points to the list
- `items` (array): Items to verify
- Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_verify_text_visible**
- Title: Verify text visible
- Description: Verify text is visible on the page. Prefer browser_verify_element_visible if possible.
- Parameters:
- `text` (string): TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}`
- Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_verify_value**
- Title: Verify value
- Description: Verify element value
- Parameters:
- `type` (string): Type of the element
- `element` (string): Human-readable element description
- `ref` (string): Exact target element reference that points to the element
- `value` (string): Value to verify. For checkbox, use "true" or "false".
- Read-only: **true**
</details>
<details>

28
config.d.ts vendored
View File

@@ -16,7 +16,7 @@
import type * as playwright from 'playwright';
export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing';
export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'verify';
export type Config = {
/**
@@ -68,12 +68,6 @@ export type Config = {
* Remote endpoint to connect to an existing Playwright server.
*/
remoteEndpoint?: string;
/**
* Paths to JavaScript files to add as initialization scripts.
* The scripts will be evaluated in every page before any of the page's scripts.
*/
initScript?: string[];
},
server?: {
@@ -86,12 +80,6 @@ export type Config = {
* The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
*/
host?: string;
/**
* The hosts this server is allowed to serve from. Defaults to the host server is bound to.
* This is not for CORS, but rather for the DNS rebinding protection.
*/
allowedHosts?: string[];
},
/**
@@ -112,19 +100,6 @@ export type Config = {
*/
saveTrace?: boolean;
/**
* If specified, saves the Playwright video of the session into the output directory.
*/
saveVideo?: {
width: number;
height: number;
};
/**
* Reuse the same browser context between all connected HTTP clients.
*/
sharedBrowserContext?: boolean;
/**
* Secrets are used to prevent LLM from getting sensitive data while
* automating scenarios such as authentication.
@@ -166,4 +141,3 @@ export type Config = {
*/
imageResponses?: 'allow' | 'omit';
};

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Playwright MCP Bridge",
"version": "0.0.41",
"version": "0.0.37",
"description": "Share browser tabs with Playwright MCP server",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB",
"permissions": [

View File

@@ -16,7 +16,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.8.2",
"vite": "^5.4.20",
"vite": "^5.0.0",
"vite-plugin-static-copy": "^3.1.1"
},
"engines": {
@@ -1796,11 +1796,10 @@
}
},
"node_modules/vite": {
"version": "5.4.20",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"version": "5.4.19",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp-extension",
"version": "0.0.41",
"version": "0.0.37",
"description": "Playwright MCP Browser Extension",
"private": true,
"repository": {
@@ -29,7 +29,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.8.2",
"vite": "^5.4.20",
"vite": "^5.0.0",
"vite-plugin-static-copy": "^3.1.1"
}
}

View File

@@ -1,142 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.auth-token-section {
margin: 16px 0;
padding: 16px;
background-color: #f6f8fa;
border-radius: 6px;
}
.auth-token-description {
font-size: 12px;
color: #656d76;
margin-bottom: 12px;
}
.auth-token-container {
display: flex;
align-items: center;
gap: 8px;
background-color: #ffffff;
padding: 8px;
}
.auth-token-code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 12px;
color: #1f2328;
border: none;
flex: 1;
padding: 0;
word-break: break-all;
}
.auth-token-refresh {
flex: none;
height: 24px;
width: 24px;
border: none;
outline: none;
color: var(--color-fg-muted);
background: transparent;
padding: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.auth-token-refresh svg {
margin: 0;
}
.auth-token-refresh:not(:disabled):hover {
background-color: var(--color-btn-selected-bg);
}
.auth-token-example-section {
margin-top: 16px;
}
.auth-token-example-toggle {
display: flex;
align-items: center;
gap: 8px;
background: none;
border: none;
padding: 8px 0;
font-size: 12px;
color: #656d76;
cursor: pointer;
outline: none;
text-align: left;
width: 100%;
}
.auth-token-example-toggle:hover {
color: #1f2328;
}
.auth-token-chevron {
display: inline-flex;
align-items: center;
justify-content: center;
transform: rotate(-90deg);
flex-shrink: 0;
}
.auth-token-chevron.expanded {
transform: rotate(0deg);
}
.auth-token-chevron svg {
width: 12px;
height: 12px;
}
.auth-token-chevron .octicon {
margin: 0px;
}
.auth-token-example-content {
margin-top: 12px;
padding: 12px 0;
}
.auth-token-example-description {
font-size: 12px;
color: #656d76;
margin-bottom: 12px;
}
.auth-token-example-config {
display: flex;
align-items: flex-start;
gap: 8px;
background-color: #ffffff;
padding: 12px;
}
.auth-token-example-code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 11px;
color: #1f2328;
white-space: pre;
flex: 1;
line-height: 1.4;
}

View File

@@ -1,118 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useCallback, useState } from 'react';
import { CopyToClipboard } from './copyToClipboard';
import * as icons from './icons';
import './authToken.css';
export const AuthTokenSection: React.FC<{}> = ({}) => {
const [authToken, setAuthToken] = useState<string>(getOrCreateAuthToken);
const [isExampleExpanded, setIsExampleExpanded] = useState<boolean>(false);
const onRegenerateToken = useCallback(() => {
const newToken = generateAuthToken();
localStorage.setItem('auth-token', newToken);
setAuthToken(newToken);
}, []);
const toggleExample = useCallback(() => {
setIsExampleExpanded(!isExampleExpanded);
}, [isExampleExpanded]);
return (
<div className='auth-token-section'>
<div className='auth-token-description'>
Set this environment variable to bypass the connection dialog:
</div>
<div className='auth-token-container'>
<code className='auth-token-code'>{authTokenCode(authToken)}</code>
<button className='auth-token-refresh' title='Generate new token' aria-label='Generate new token'onClick={onRegenerateToken}>{icons.refresh()}</button>
<CopyToClipboard value={authTokenCode(authToken)} />
</div>
<div className='auth-token-example-section'>
<button
className='auth-token-example-toggle'
onClick={toggleExample}
aria-expanded={isExampleExpanded}
title={isExampleExpanded ? 'Hide example config' : 'Show example config'}
>
<span className={`auth-token-chevron ${isExampleExpanded ? 'expanded' : ''}`}>
{icons.chevronDown()}
</span>
Example MCP server configuration
</button>
{isExampleExpanded && (
<div className='auth-token-example-content'>
<div className='auth-token-example-description'>
Add this configuration to your MCP client (e.g., VS Code) to connect to the Playwright MCP Bridge:
</div>
<div className='auth-token-example-config'>
<code className='auth-token-example-code'>{exampleConfig(authToken)}</code>
<CopyToClipboard value={exampleConfig(authToken)} />
</div>
</div>
)}
</div>
</div>
);
};
function authTokenCode(authToken: string) {
return `PLAYWRIGHT_MCP_EXTENSION_TOKEN=${authToken}`;
}
function exampleConfig(authToken: string) {
return `{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--extension"],
"env": {
"PLAYWRIGHT_MCP_EXTENSION_TOKEN":
"${authToken}"
}
}
}
}`;
}
function generateAuthToken(): string {
// Generate a cryptographically secure random token
const array = new Uint8Array(32);
crypto.getRandomValues(array);
// Convert to base64 and make it URL-safe
return btoa(String.fromCharCode.apply(null, Array.from(array)))
.replace(/[+/=]/g, match => {
switch (match) {
case '+': return '-';
case '/': return '_';
case '=': return '';
default: return match;
}
});
}
export const getOrCreateAuthToken = (): string => {
let token = localStorage.getItem('auth-token');
if (!token) {
token = generateAuthToken();
localStorage.setItem('auth-token', token);
}
return token;
}

View File

@@ -1,891 +0,0 @@
/* The MIT License (MIT)
Copyright (c) 2021 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
:root {
--color-canvas-default-transparent: rgba(255,255,255,0);
--color-marketing-icon-primary: #218bff;
--color-marketing-icon-secondary: #54aeff;
--color-diff-blob-addition-num-text: #24292f;
--color-diff-blob-addition-fg: #24292f;
--color-diff-blob-addition-num-bg: #CCFFD8;
--color-diff-blob-addition-line-bg: #E6FFEC;
--color-diff-blob-addition-word-bg: #ABF2BC;
--color-diff-blob-deletion-num-text: #24292f;
--color-diff-blob-deletion-fg: #24292f;
--color-diff-blob-deletion-num-bg: #FFD7D5;
--color-diff-blob-deletion-line-bg: #FFEBE9;
--color-diff-blob-deletion-word-bg: rgba(255,129,130,0.4);
--color-diff-blob-hunk-num-bg: rgba(84,174,255,0.4);
--color-diff-blob-expander-icon: #57606a;
--color-diff-blob-selected-line-highlight-mix-blend-mode: multiply;
--color-diffstat-deletion-border: rgba(27,31,36,0.15);
--color-diffstat-addition-border: rgba(27,31,36,0.15);
--color-diffstat-addition-bg: #2da44e;
--color-search-keyword-hl: #fff8c5;
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #8250df;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #FFEBE9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-codemirror-text: #24292f;
--color-codemirror-bg: #ffffff;
--color-codemirror-gutters-bg: #ffffff;
--color-codemirror-guttermarker-text: #ffffff;
--color-codemirror-guttermarker-subtle-text: #6e7781;
--color-codemirror-linenumber-text: #57606a;
--color-codemirror-cursor: #24292f;
--color-codemirror-selection-bg: rgba(84,174,255,0.4);
--color-codemirror-activeline-bg: rgba(234,238,242,0.5);
--color-codemirror-matchingbracket-text: #24292f;
--color-codemirror-lines-bg: #ffffff;
--color-codemirror-syntax-comment: #24292f;
--color-codemirror-syntax-constant: #0550ae;
--color-codemirror-syntax-entity: #8250df;
--color-codemirror-syntax-keyword: #cf222e;
--color-codemirror-syntax-storage: #cf222e;
--color-codemirror-syntax-string: #0a3069;
--color-codemirror-syntax-support: #0550ae;
--color-codemirror-syntax-variable: #953800;
--color-checks-bg: #24292f;
--color-checks-run-border-width: 0px;
--color-checks-container-border-width: 0px;
--color-checks-text-primary: #f6f8fa;
--color-checks-text-secondary: #8c959f;
--color-checks-text-link: #54aeff;
--color-checks-btn-icon: #afb8c1;
--color-checks-btn-hover-icon: #f6f8fa;
--color-checks-btn-hover-bg: rgba(255,255,255,0.125);
--color-checks-input-text: #eaeef2;
--color-checks-input-placeholder-text: #8c959f;
--color-checks-input-focus-text: #8c959f;
--color-checks-input-bg: #32383f;
--color-checks-input-shadow: none;
--color-checks-donut-error: #fa4549;
--color-checks-donut-pending: #bf8700;
--color-checks-donut-success: #2da44e;
--color-checks-donut-neutral: #afb8c1;
--color-checks-dropdown-text: #afb8c1;
--color-checks-dropdown-bg: #32383f;
--color-checks-dropdown-border: #424a53;
--color-checks-dropdown-shadow: rgba(27,31,36,0.3);
--color-checks-dropdown-hover-text: #f6f8fa;
--color-checks-dropdown-hover-bg: #424a53;
--color-checks-dropdown-btn-hover-text: #f6f8fa;
--color-checks-dropdown-btn-hover-bg: #32383f;
--color-checks-scrollbar-thumb-bg: #57606a;
--color-checks-header-label-text: #d0d7de;
--color-checks-header-label-open-text: #f6f8fa;
--color-checks-header-border: #32383f;
--color-checks-header-icon: #8c959f;
--color-checks-line-text: #d0d7de;
--color-checks-line-num-text: rgba(140,149,159,0.75);
--color-checks-line-timestamp-text: #8c959f;
--color-checks-line-hover-bg: #32383f;
--color-checks-line-selected-bg: rgba(33,139,255,0.15);
--color-checks-line-selected-num-text: #54aeff;
--color-checks-line-dt-fm-text: #24292f;
--color-checks-line-dt-fm-bg: #9a6700;
--color-checks-gate-bg: rgba(125,78,0,0.15);
--color-checks-gate-text: #d0d7de;
--color-checks-gate-waiting-text: #afb8c1;
--color-checks-step-header-open-bg: #32383f;
--color-checks-step-error-text: #ff8182;
--color-checks-step-warning-text: #d4a72c;
--color-checks-logline-text: #8c959f;
--color-checks-logline-num-text: rgba(140,149,159,0.75);
--color-checks-logline-debug-text: #c297ff;
--color-checks-logline-error-text: #d0d7de;
--color-checks-logline-error-num-text: #ff8182;
--color-checks-logline-error-bg: rgba(164,14,38,0.15);
--color-checks-logline-warning-text: #d0d7de;
--color-checks-logline-warning-num-text: #d4a72c;
--color-checks-logline-warning-bg: rgba(125,78,0,0.15);
--color-checks-logline-command-text: #54aeff;
--color-checks-logline-section-text: #4ac26b;
--color-checks-ansi-black: #24292f;
--color-checks-ansi-black-bright: #32383f;
--color-checks-ansi-white: #d0d7de;
--color-checks-ansi-white-bright: #d0d7de;
--color-checks-ansi-gray: #8c959f;
--color-checks-ansi-red: #ff8182;
--color-checks-ansi-red-bright: #ffaba8;
--color-checks-ansi-green: #4ac26b;
--color-checks-ansi-green-bright: #6fdd8b;
--color-checks-ansi-yellow: #d4a72c;
--color-checks-ansi-yellow-bright: #eac54f;
--color-checks-ansi-blue: #54aeff;
--color-checks-ansi-blue-bright: #80ccff;
--color-checks-ansi-magenta: #c297ff;
--color-checks-ansi-magenta-bright: #d8b9ff;
--color-checks-ansi-cyan: #76e3ea;
--color-checks-ansi-cyan-bright: #b3f0ff;
--color-project-header-bg: #24292f;
--color-project-sidebar-bg: #ffffff;
--color-project-gradient-in: #ffffff;
--color-project-gradient-out: rgba(255,255,255,0);
--color-mktg-success: rgba(36,146,67,1);
--color-mktg-info: rgba(19,119,234,1);
--color-mktg-bg-shade-gradient-top: rgba(27,31,36,0.065);
--color-mktg-bg-shade-gradient-bottom: rgba(27,31,36,0);
--color-mktg-btn-bg-top: hsla(228,82%,66%,1);
--color-mktg-btn-bg-bottom: #4969ed;
--color-mktg-btn-bg-overlay-top: hsla(228,74%,59%,1);
--color-mktg-btn-bg-overlay-bottom: #3355e0;
--color-mktg-btn-text: #ffffff;
--color-mktg-btn-primary-bg-top: hsla(137,56%,46%,1);
--color-mktg-btn-primary-bg-bottom: #2ea44f;
--color-mktg-btn-primary-bg-overlay-top: hsla(134,60%,38%,1);
--color-mktg-btn-primary-bg-overlay-bottom: #22863a;
--color-mktg-btn-primary-text: #ffffff;
--color-mktg-btn-enterprise-bg-top: hsla(249,100%,72%,1);
--color-mktg-btn-enterprise-bg-bottom: #6f57ff;
--color-mktg-btn-enterprise-bg-overlay-top: hsla(248,65%,63%,1);
--color-mktg-btn-enterprise-bg-overlay-bottom: #614eda;
--color-mktg-btn-enterprise-text: #ffffff;
--color-mktg-btn-outline-text: #4969ed;
--color-mktg-btn-outline-border: rgba(73,105,237,0.3);
--color-mktg-btn-outline-hover-text: #3355e0;
--color-mktg-btn-outline-hover-border: rgba(51,85,224,0.5);
--color-mktg-btn-outline-focus-border: #4969ed;
--color-mktg-btn-outline-focus-border-inset: rgba(73,105,237,0.5);
--color-mktg-btn-dark-text: #ffffff;
--color-mktg-btn-dark-border: rgba(255,255,255,0.3);
--color-mktg-btn-dark-hover-text: #ffffff;
--color-mktg-btn-dark-hover-border: rgba(255,255,255,0.5);
--color-mktg-btn-dark-focus-border: #ffffff;
--color-mktg-btn-dark-focus-border-inset: rgba(255,255,255,0.5);
--color-avatar-bg: #ffffff;
--color-avatar-border: rgba(27,31,36,0.15);
--color-avatar-stack-fade: #afb8c1;
--color-avatar-stack-fade-more: #d0d7de;
--color-avatar-child-shadow: -2px -2px 0 rgba(255,255,255,0.8);
--color-topic-tag-border: rgba(0,0,0,0);
--color-select-menu-backdrop-border: rgba(0,0,0,0);
--color-select-menu-tap-highlight: rgba(175,184,193,0.5);
--color-select-menu-tap-focus-bg: #b6e3ff;
--color-overlay-shadow: 0 1px 3px rgba(27,31,36,0.12), 0 8px 24px rgba(66,74,83,0.12);
--color-header-text: rgba(255,255,255,0.7);
--color-header-bg: #24292f;
--color-header-logo: #ffffff;
--color-header-search-bg: #24292f;
--color-header-search-border: #57606a;
--color-sidenav-selected-bg: #ffffff;
--color-menu-bg-active: rgba(0,0,0,0);
--color-control-transparent-bg-hover: #818b981a;
--color-input-disabled-bg: rgba(175,184,193,0.2);
--color-timeline-badge-bg: #eaeef2;
--color-ansi-black: #24292f;
--color-ansi-black-bright: #57606a;
--color-ansi-white: #6e7781;
--color-ansi-white-bright: #8c959f;
--color-ansi-gray: #6e7781;
--color-ansi-red: #cf222e;
--color-ansi-red-bright: #a40e26;
--color-ansi-green: #116329;
--color-ansi-green-bright: #1a7f37;
--color-ansi-yellow: #4d2d00;
--color-ansi-yellow-bright: #633c01;
--color-ansi-blue: #0969da;
--color-ansi-blue-bright: #218bff;
--color-ansi-magenta: #8250df;
--color-ansi-magenta-bright: #a475f9;
--color-ansi-cyan: #1b7c83;
--color-ansi-cyan-bright: #3192aa;
--color-btn-text: #24292f;
--color-btn-bg: #f6f8fa;
--color-btn-border: rgba(27,31,36,0.15);
--color-btn-shadow: 0 1px 0 rgba(27,31,36,0.04);
--color-btn-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.25);
--color-btn-hover-bg: #f3f4f6;
--color-btn-hover-border: rgba(27,31,36,0.15);
--color-btn-active-bg: hsla(220,14%,93%,1);
--color-btn-active-border: rgba(27,31,36,0.15);
--color-btn-selected-bg: hsla(220,14%,94%,1);
--color-btn-focus-bg: #f6f8fa;
--color-btn-focus-border: rgba(27,31,36,0.15);
--color-btn-focus-shadow: 0 0 0 3px rgba(9,105,218,0.3);
--color-btn-shadow-active: inset 0 0.15em 0.3em rgba(27,31,36,0.15);
--color-btn-shadow-input-focus: 0 0 0 0.2em rgba(9,105,218,0.3);
--color-btn-counter-bg: rgba(27,31,36,0.08);
--color-btn-primary-text: #ffffff;
--color-btn-primary-bg: #2da44e;
--color-btn-primary-border: rgba(27,31,36,0.15);
--color-btn-primary-shadow: 0 1px 0 rgba(27,31,36,0.1);
--color-btn-primary-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
--color-btn-primary-hover-bg: #2c974b;
--color-btn-primary-hover-border: rgba(27,31,36,0.15);
--color-btn-primary-selected-bg: hsla(137,55%,36%,1);
--color-btn-primary-selected-shadow: inset 0 1px 0 rgba(0,45,17,0.2);
--color-btn-primary-disabled-text: rgba(255,255,255,0.8);
--color-btn-primary-disabled-bg: #94d3a2;
--color-btn-primary-disabled-border: rgba(27,31,36,0.15);
--color-btn-primary-focus-bg: #2da44e;
--color-btn-primary-focus-border: rgba(27,31,36,0.15);
--color-btn-primary-focus-shadow: 0 0 0 3px rgba(45,164,78,0.4);
--color-btn-primary-icon: rgba(255,255,255,0.8);
--color-btn-primary-counter-bg: rgba(255,255,255,0.2);
--color-btn-outline-text: #0969da;
--color-btn-outline-hover-text: #ffffff;
--color-btn-outline-hover-bg: #0969da;
--color-btn-outline-hover-border: rgba(27,31,36,0.15);
--color-btn-outline-hover-shadow: 0 1px 0 rgba(27,31,36,0.1);
--color-btn-outline-hover-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
--color-btn-outline-hover-counter-bg: rgba(255,255,255,0.2);
--color-btn-outline-selected-text: #ffffff;
--color-btn-outline-selected-bg: hsla(212,92%,42%,1);
--color-btn-outline-selected-border: rgba(27,31,36,0.15);
--color-btn-outline-selected-shadow: inset 0 1px 0 rgba(0,33,85,0.2);
--color-btn-outline-disabled-text: rgba(9,105,218,0.5);
--color-btn-outline-disabled-bg: #f6f8fa;
--color-btn-outline-disabled-counter-bg: rgba(9,105,218,0.05);
--color-btn-outline-focus-border: rgba(27,31,36,0.15);
--color-btn-outline-focus-shadow: 0 0 0 3px rgba(5,80,174,0.4);
--color-btn-outline-counter-bg: rgba(9,105,218,0.1);
--color-btn-danger-text: #cf222e;
--color-btn-danger-hover-text: #ffffff;
--color-btn-danger-hover-bg: #a40e26;
--color-btn-danger-hover-border: rgba(27,31,36,0.15);
--color-btn-danger-hover-shadow: 0 1px 0 rgba(27,31,36,0.1);
--color-btn-danger-hover-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
--color-btn-danger-hover-counter-bg: rgba(255,255,255,0.2);
--color-btn-danger-selected-text: #ffffff;
--color-btn-danger-selected-bg: hsla(356,72%,44%,1);
--color-btn-danger-selected-border: rgba(27,31,36,0.15);
--color-btn-danger-selected-shadow: inset 0 1px 0 rgba(76,0,20,0.2);
--color-btn-danger-disabled-text: rgba(207,34,46,0.5);
--color-btn-danger-disabled-bg: #f6f8fa;
--color-btn-danger-disabled-counter-bg: rgba(207,34,46,0.05);
--color-btn-danger-focus-border: rgba(27,31,36,0.15);
--color-btn-danger-focus-shadow: 0 0 0 3px rgba(164,14,38,0.4);
--color-btn-danger-counter-bg: rgba(207,34,46,0.1);
--color-btn-danger-icon: #cf222e;
--color-btn-danger-hover-icon: #ffffff;
--color-underlinenav-icon: #6e7781;
--color-underlinenav-border-hover: rgba(175,184,193,0.2);
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-fg-subtle: #6e7781;
--color-fg-on-emphasis: #ffffff;
--color-canvas-default: #ffffff;
--color-canvas-overlay: #ffffff;
--color-canvas-inset: #f6f8fa;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210,18%,87%,1);
--color-border-subtle: rgba(27,31,36,0.15);
--color-shadow-small: 0 1px 0 rgba(27,31,36,0.04);
--color-shadow-medium: 0 3px 6px rgba(140,149,159,0.15);
--color-shadow-large: 0 8px 24px rgba(140,149,159,0.2);
--color-shadow-extra-large: 0 12px 28px rgba(140,149,159,0.3);
--color-neutral-emphasis-plus: #24292f;
--color-neutral-emphasis: #6e7781;
--color-neutral-muted: rgba(175,184,193,0.2);
--color-neutral-subtle: rgba(234,238,242,0.5);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-accent-muted: rgba(84,174,255,0.4);
--color-accent-subtle: #ddf4ff;
--color-success-fg: #1a7f37;
--color-success-emphasis: #2da44e;
--color-success-muted: rgba(74,194,107,0.4);
--color-success-subtle: #dafbe1;
--color-attention-fg: #9a6700;
--color-attention-emphasis: #bf8700;
--color-attention-muted: rgba(212,167,44,0.4);
--color-attention-subtle: #fff8c5;
--color-severe-fg: #bc4c00;
--color-severe-emphasis: #bc4c00;
--color-severe-muted: rgba(251,143,68,0.4);
--color-severe-subtle: #fff1e5;
--color-danger-fg: #cf222e;
--color-danger-emphasis: #cf222e;
--color-danger-muted: rgba(255,129,130,0.4);
--color-danger-subtle: #FFEBE9;
--color-done-fg: #8250df;
--color-done-emphasis: #8250df;
--color-done-muted: rgba(194,151,255,0.4);
--color-done-subtle: #fbefff;
--color-sponsors-fg: #bf3989;
--color-sponsors-emphasis: #bf3989;
--color-sponsors-muted: rgba(255,128,200,0.4);
--color-sponsors-subtle: #ffeff7;
--color-primer-canvas-backdrop: rgba(27,31,36,0.5);
--color-primer-canvas-sticky: rgba(255,255,255,0.95);
--color-primer-border-active: #FD8C73;
--color-primer-border-contrast: rgba(27,31,36,0.1);
--color-primer-shadow-highlight: inset 0 1px 0 rgba(255,255,255,0.25);
--color-primer-shadow-inset: inset 0 1px 0 rgba(208,215,222,0.2);
--color-primer-shadow-focus: 0 0 0 3px rgba(9,105,218,0.3);
--color-scale-black: #1b1f24;
--color-scale-white: #ffffff;
--color-scale-gray-0: #f6f8fa;
--color-scale-gray-1: #eaeef2;
--color-scale-gray-2: #d0d7de;
--color-scale-gray-3: #afb8c1;
--color-scale-gray-4: #8c959f;
--color-scale-gray-5: #6e7781;
--color-scale-gray-6: #57606a;
--color-scale-gray-7: #424a53;
--color-scale-gray-8: #32383f;
--color-scale-gray-9: #24292f;
--color-scale-blue-0: #ddf4ff;
--color-scale-blue-1: #b6e3ff;
--color-scale-blue-2: #80ccff;
--color-scale-blue-3: #54aeff;
--color-scale-blue-4: #218bff;
--color-scale-blue-5: #0969da;
--color-scale-blue-6: #0550ae;
--color-scale-blue-7: #033d8b;
--color-scale-blue-8: #0a3069;
--color-scale-blue-9: #002155;
--color-scale-green-0: #dafbe1;
--color-scale-green-1: #aceebb;
--color-scale-green-2: #6fdd8b;
--color-scale-green-3: #4ac26b;
--color-scale-green-4: #2da44e;
--color-scale-green-5: #1a7f37;
--color-scale-green-6: #116329;
--color-scale-green-7: #044f1e;
--color-scale-green-8: #003d16;
--color-scale-green-9: #002d11;
--color-scale-yellow-0: #fff8c5;
--color-scale-yellow-1: #fae17d;
--color-scale-yellow-2: #eac54f;
--color-scale-yellow-3: #d4a72c;
--color-scale-yellow-4: #bf8700;
--color-scale-yellow-5: #9a6700;
--color-scale-yellow-6: #7d4e00;
--color-scale-yellow-7: #633c01;
--color-scale-yellow-8: #4d2d00;
--color-scale-yellow-9: #3b2300;
--color-scale-orange-0: #fff1e5;
--color-scale-orange-1: #ffd8b5;
--color-scale-orange-2: #ffb77c;
--color-scale-orange-3: #fb8f44;
--color-scale-orange-4: #e16f24;
--color-scale-orange-5: #bc4c00;
--color-scale-orange-6: #953800;
--color-scale-orange-7: #762c00;
--color-scale-orange-8: #5c2200;
--color-scale-orange-9: #471700;
--color-scale-red-0: #FFEBE9;
--color-scale-red-1: #ffcecb;
--color-scale-red-2: #ffaba8;
--color-scale-red-3: #ff8182;
--color-scale-red-4: #fa4549;
--color-scale-red-5: #cf222e;
--color-scale-red-6: #a40e26;
--color-scale-red-7: #82071e;
--color-scale-red-8: #660018;
--color-scale-red-9: #4c0014;
--color-scale-purple-0: #fbefff;
--color-scale-purple-1: #ecd8ff;
--color-scale-purple-2: #d8b9ff;
--color-scale-purple-3: #c297ff;
--color-scale-purple-4: #a475f9;
--color-scale-purple-5: #8250df;
--color-scale-purple-6: #6639ba;
--color-scale-purple-7: #512a97;
--color-scale-purple-8: #3e1f79;
--color-scale-purple-9: #2e1461;
--color-scale-pink-0: #ffeff7;
--color-scale-pink-1: #ffd3eb;
--color-scale-pink-2: #ffadda;
--color-scale-pink-3: #ff80c8;
--color-scale-pink-4: #e85aad;
--color-scale-pink-5: #bf3989;
--color-scale-pink-6: #99286e;
--color-scale-pink-7: #772057;
--color-scale-pink-8: #611347;
--color-scale-pink-9: #4d0336;
--color-scale-coral-0: #FFF0EB;
--color-scale-coral-1: #FFD6CC;
--color-scale-coral-2: #FFB4A1;
--color-scale-coral-3: #FD8C73;
--color-scale-coral-4: #EC6547;
--color-scale-coral-5: #C4432B;
--color-scale-coral-6: #9E2F1C;
--color-scale-coral-7: #801F0F;
--color-scale-coral-8: #691105;
--color-scale-coral-9: #510901
}
@media(prefers-color-scheme: dark) {
:root {
--color-canvas-default-transparent: rgba(13,17,23,0);
--color-marketing-icon-primary: #79c0ff;
--color-marketing-icon-secondary: #1f6feb;
--color-diff-blob-addition-num-text: #c9d1d9;
--color-diff-blob-addition-fg: #c9d1d9;
--color-diff-blob-addition-num-bg: rgba(63,185,80,0.3);
--color-diff-blob-addition-line-bg: rgba(46,160,67,0.15);
--color-diff-blob-addition-word-bg: rgba(46,160,67,0.4);
--color-diff-blob-deletion-num-text: #c9d1d9;
--color-diff-blob-deletion-fg: #c9d1d9;
--color-diff-blob-deletion-num-bg: rgba(248,81,73,0.3);
--color-diff-blob-deletion-line-bg: rgba(248,81,73,0.15);
--color-diff-blob-deletion-word-bg: rgba(248,81,73,0.4);
--color-diff-blob-hunk-num-bg: rgba(56,139,253,0.4);
--color-diff-blob-expander-icon: #8b949e;
--color-diff-blob-selected-line-highlight-mix-blend-mode: screen;
--color-diffstat-deletion-border: rgba(240,246,252,0.1);
--color-diffstat-addition-border: rgba(240,246,252,0.1);
--color-diffstat-addition-bg: #3fb950;
--color-search-keyword-hl: rgba(210,153,34,0.4);
--color-prettylights-syntax-comment: #8b949e;
--color-prettylights-syntax-constant: #79c0ff;
--color-prettylights-syntax-entity: #d2a8ff;
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
--color-prettylights-syntax-entity-tag: #7ee787;
--color-prettylights-syntax-keyword: #ff7b72;
--color-prettylights-syntax-string: #a5d6ff;
--color-prettylights-syntax-variable: #ffa657;
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
--color-prettylights-syntax-carriage-return-bg: #b62324;
--color-prettylights-syntax-string-regexp: #7ee787;
--color-prettylights-syntax-markup-list: #f2cc60;
--color-prettylights-syntax-markup-heading: #1f6feb;
--color-prettylights-syntax-markup-italic: #c9d1d9;
--color-prettylights-syntax-markup-bold: #c9d1d9;
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
--color-prettylights-syntax-markup-deleted-bg: #67060c;
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
--color-prettylights-syntax-markup-inserted-bg: #033a16;
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
--color-codemirror-text: #c9d1d9;
--color-codemirror-bg: #0d1117;
--color-codemirror-gutters-bg: #0d1117;
--color-codemirror-guttermarker-text: #0d1117;
--color-codemirror-guttermarker-subtle-text: #484f58;
--color-codemirror-linenumber-text: #8b949e;
--color-codemirror-cursor: #c9d1d9;
--color-codemirror-selection-bg: rgba(56,139,253,0.4);
--color-codemirror-activeline-bg: rgba(110,118,129,0.1);
--color-codemirror-matchingbracket-text: #c9d1d9;
--color-codemirror-lines-bg: #0d1117;
--color-codemirror-syntax-comment: #8b949e;
--color-codemirror-syntax-constant: #79c0ff;
--color-codemirror-syntax-entity: #d2a8ff;
--color-codemirror-syntax-keyword: #ff7b72;
--color-codemirror-syntax-storage: #ff7b72;
--color-codemirror-syntax-string: #a5d6ff;
--color-codemirror-syntax-support: #79c0ff;
--color-codemirror-syntax-variable: #ffa657;
--color-checks-bg: #010409;
--color-checks-run-border-width: 1px;
--color-checks-container-border-width: 1px;
--color-checks-text-primary: #c9d1d9;
--color-checks-text-secondary: #8b949e;
--color-checks-text-link: #58a6ff;
--color-checks-btn-icon: #8b949e;
--color-checks-btn-hover-icon: #c9d1d9;
--color-checks-btn-hover-bg: rgba(110,118,129,0.1);
--color-checks-input-text: #8b949e;
--color-checks-input-placeholder-text: #484f58;
--color-checks-input-focus-text: #c9d1d9;
--color-checks-input-bg: #161b22;
--color-checks-input-shadow: none;
--color-checks-donut-error: #f85149;
--color-checks-donut-pending: #d29922;
--color-checks-donut-success: #2ea043;
--color-checks-donut-neutral: #8b949e;
--color-checks-dropdown-text: #c9d1d9;
--color-checks-dropdown-bg: #161b22;
--color-checks-dropdown-border: #30363d;
--color-checks-dropdown-shadow: rgba(1,4,9,0.3);
--color-checks-dropdown-hover-text: #c9d1d9;
--color-checks-dropdown-hover-bg: rgba(110,118,129,0.1);
--color-checks-dropdown-btn-hover-text: #c9d1d9;
--color-checks-dropdown-btn-hover-bg: rgba(110,118,129,0.1);
--color-checks-scrollbar-thumb-bg: rgba(110,118,129,0.4);
--color-checks-header-label-text: #8b949e;
--color-checks-header-label-open-text: #c9d1d9;
--color-checks-header-border: #21262d;
--color-checks-header-icon: #8b949e;
--color-checks-line-text: #8b949e;
--color-checks-line-num-text: #484f58;
--color-checks-line-timestamp-text: #484f58;
--color-checks-line-hover-bg: rgba(110,118,129,0.1);
--color-checks-line-selected-bg: rgba(56,139,253,0.15);
--color-checks-line-selected-num-text: #58a6ff;
--color-checks-line-dt-fm-text: #f0f6fc;
--color-checks-line-dt-fm-bg: #9e6a03;
--color-checks-gate-bg: rgba(187,128,9,0.15);
--color-checks-gate-text: #8b949e;
--color-checks-gate-waiting-text: #d29922;
--color-checks-step-header-open-bg: #161b22;
--color-checks-step-error-text: #f85149;
--color-checks-step-warning-text: #d29922;
--color-checks-logline-text: #8b949e;
--color-checks-logline-num-text: #484f58;
--color-checks-logline-debug-text: #a371f7;
--color-checks-logline-error-text: #8b949e;
--color-checks-logline-error-num-text: #484f58;
--color-checks-logline-error-bg: rgba(248,81,73,0.15);
--color-checks-logline-warning-text: #8b949e;
--color-checks-logline-warning-num-text: #d29922;
--color-checks-logline-warning-bg: rgba(187,128,9,0.15);
--color-checks-logline-command-text: #58a6ff;
--color-checks-logline-section-text: #3fb950;
--color-checks-ansi-black: #0d1117;
--color-checks-ansi-black-bright: #161b22;
--color-checks-ansi-white: #b1bac4;
--color-checks-ansi-white-bright: #b1bac4;
--color-checks-ansi-gray: #6e7681;
--color-checks-ansi-red: #ff7b72;
--color-checks-ansi-red-bright: #ffa198;
--color-checks-ansi-green: #3fb950;
--color-checks-ansi-green-bright: #56d364;
--color-checks-ansi-yellow: #d29922;
--color-checks-ansi-yellow-bright: #e3b341;
--color-checks-ansi-blue: #58a6ff;
--color-checks-ansi-blue-bright: #79c0ff;
--color-checks-ansi-magenta: #bc8cff;
--color-checks-ansi-magenta-bright: #d2a8ff;
--color-checks-ansi-cyan: #76e3ea;
--color-checks-ansi-cyan-bright: #b3f0ff;
--color-project-header-bg: #0d1117;
--color-project-sidebar-bg: #161b22;
--color-project-gradient-in: #161b22;
--color-project-gradient-out: rgba(22,27,34,0);
--color-mktg-success: rgba(41,147,61,1);
--color-mktg-info: rgba(42,123,243,1);
--color-mktg-bg-shade-gradient-top: rgba(1,4,9,0.065);
--color-mktg-bg-shade-gradient-bottom: rgba(1,4,9,0);
--color-mktg-btn-bg-top: hsla(228,82%,66%,1);
--color-mktg-btn-bg-bottom: #4969ed;
--color-mktg-btn-bg-overlay-top: hsla(228,74%,59%,1);
--color-mktg-btn-bg-overlay-bottom: #3355e0;
--color-mktg-btn-text: #f0f6fc;
--color-mktg-btn-primary-bg-top: hsla(137,56%,46%,1);
--color-mktg-btn-primary-bg-bottom: #2ea44f;
--color-mktg-btn-primary-bg-overlay-top: hsla(134,60%,38%,1);
--color-mktg-btn-primary-bg-overlay-bottom: #22863a;
--color-mktg-btn-primary-text: #f0f6fc;
--color-mktg-btn-enterprise-bg-top: hsla(249,100%,72%,1);
--color-mktg-btn-enterprise-bg-bottom: #6f57ff;
--color-mktg-btn-enterprise-bg-overlay-top: hsla(248,65%,63%,1);
--color-mktg-btn-enterprise-bg-overlay-bottom: #614eda;
--color-mktg-btn-enterprise-text: #f0f6fc;
--color-mktg-btn-outline-text: #f0f6fc;
--color-mktg-btn-outline-border: rgba(240,246,252,0.3);
--color-mktg-btn-outline-hover-text: #f0f6fc;
--color-mktg-btn-outline-hover-border: rgba(240,246,252,0.5);
--color-mktg-btn-outline-focus-border: #f0f6fc;
--color-mktg-btn-outline-focus-border-inset: rgba(240,246,252,0.5);
--color-mktg-btn-dark-text: #f0f6fc;
--color-mktg-btn-dark-border: rgba(240,246,252,0.3);
--color-mktg-btn-dark-hover-text: #f0f6fc;
--color-mktg-btn-dark-hover-border: rgba(240,246,252,0.5);
--color-mktg-btn-dark-focus-border: #f0f6fc;
--color-mktg-btn-dark-focus-border-inset: rgba(240,246,252,0.5);
--color-avatar-bg: rgba(240,246,252,0.1);
--color-avatar-border: rgba(240,246,252,0.1);
--color-avatar-stack-fade: #30363d;
--color-avatar-stack-fade-more: #21262d;
--color-avatar-child-shadow: -2px -2px 0 #0d1117;
--color-topic-tag-border: rgba(0,0,0,0);
--color-select-menu-backdrop-border: #484f58;
--color-select-menu-tap-highlight: rgba(48,54,61,0.5);
--color-select-menu-tap-focus-bg: #0c2d6b;
--color-overlay-shadow: 0 0 0 1px #30363d, 0 16px 32px rgba(1,4,9,0.85);
--color-header-text: rgba(240,246,252,0.7);
--color-header-bg: #161b22;
--color-header-logo: #f0f6fc;
--color-header-search-bg: #0d1117;
--color-header-search-border: #30363d;
--color-sidenav-selected-bg: #21262d;
--color-menu-bg-active: #161b22;
--color-control-transparent-bg-hover: #656c7633;
--color-input-disabled-bg: rgba(110,118,129,0);
--color-timeline-badge-bg: #21262d;
--color-ansi-black: #484f58;
--color-ansi-black-bright: #6e7681;
--color-ansi-white: #b1bac4;
--color-ansi-white-bright: #f0f6fc;
--color-ansi-gray: #6e7681;
--color-ansi-red: #ff7b72;
--color-ansi-red-bright: #ffa198;
--color-ansi-green: #3fb950;
--color-ansi-green-bright: #56d364;
--color-ansi-yellow: #d29922;
--color-ansi-yellow-bright: #e3b341;
--color-ansi-blue: #58a6ff;
--color-ansi-blue-bright: #79c0ff;
--color-ansi-magenta: #bc8cff;
--color-ansi-magenta-bright: #d2a8ff;
--color-ansi-cyan: #39c5cf;
--color-ansi-cyan-bright: #56d4dd;
--color-btn-text: #c9d1d9;
--color-btn-bg: #21262d;
--color-btn-border: rgba(240,246,252,0.1);
--color-btn-shadow: 0 0 transparent;
--color-btn-inset-shadow: 0 0 transparent;
--color-btn-hover-bg: #30363d;
--color-btn-hover-border: #8b949e;
--color-btn-active-bg: hsla(212,12%,18%,1);
--color-btn-active-border: #6e7681;
--color-btn-selected-bg: #161b22;
--color-btn-focus-bg: #21262d;
--color-btn-focus-border: #8b949e;
--color-btn-focus-shadow: 0 0 0 3px rgba(139,148,158,0.3);
--color-btn-shadow-active: inset 0 0.15em 0.3em rgba(1,4,9,0.15);
--color-btn-shadow-input-focus: 0 0 0 0.2em rgba(31,111,235,0.3);
--color-btn-counter-bg: #30363d;
--color-btn-primary-text: #ffffff;
--color-btn-primary-bg: #238636;
--color-btn-primary-border: rgba(240,246,252,0.1);
--color-btn-primary-shadow: 0 0 transparent;
--color-btn-primary-inset-shadow: 0 0 transparent;
--color-btn-primary-hover-bg: #2ea043;
--color-btn-primary-hover-border: rgba(240,246,252,0.1);
--color-btn-primary-selected-bg: #238636;
--color-btn-primary-selected-shadow: 0 0 transparent;
--color-btn-primary-disabled-text: rgba(240,246,252,0.5);
--color-btn-primary-disabled-bg: rgba(35,134,54,0.6);
--color-btn-primary-disabled-border: rgba(240,246,252,0.1);
--color-btn-primary-focus-bg: #238636;
--color-btn-primary-focus-border: rgba(240,246,252,0.1);
--color-btn-primary-focus-shadow: 0 0 0 3px rgba(46,164,79,0.4);
--color-btn-primary-icon: #f0f6fc;
--color-btn-primary-counter-bg: rgba(240,246,252,0.2);
--color-btn-outline-text: #58a6ff;
--color-btn-outline-hover-text: #58a6ff;
--color-btn-outline-hover-bg: #30363d;
--color-btn-outline-hover-border: rgba(240,246,252,0.1);
--color-btn-outline-hover-shadow: 0 1px 0 rgba(1,4,9,0.1);
--color-btn-outline-hover-inset-shadow: inset 0 1px 0 rgba(240,246,252,0.03);
--color-btn-outline-hover-counter-bg: rgba(240,246,252,0.2);
--color-btn-outline-selected-text: #f0f6fc;
--color-btn-outline-selected-bg: #0d419d;
--color-btn-outline-selected-border: rgba(240,246,252,0.1);
--color-btn-outline-selected-shadow: 0 0 transparent;
--color-btn-outline-disabled-text: rgba(88,166,255,0.5);
--color-btn-outline-disabled-bg: #0d1117;
--color-btn-outline-disabled-counter-bg: rgba(31,111,235,0.05);
--color-btn-outline-focus-border: rgba(240,246,252,0.1);
--color-btn-outline-focus-shadow: 0 0 0 3px rgba(17,88,199,0.4);
--color-btn-outline-counter-bg: rgba(31,111,235,0.1);
--color-btn-danger-text: #f85149;
--color-btn-danger-hover-text: #f0f6fc;
--color-btn-danger-hover-bg: #da3633;
--color-btn-danger-hover-border: #f85149;
--color-btn-danger-hover-shadow: 0 0 transparent;
--color-btn-danger-hover-inset-shadow: 0 0 transparent;
--color-btn-danger-hover-icon: #f0f6fc;
--color-btn-danger-hover-counter-bg: rgba(255,255,255,0.2);
--color-btn-danger-selected-text: #ffffff;
--color-btn-danger-selected-bg: #b62324;
--color-btn-danger-selected-border: #ff7b72;
--color-btn-danger-selected-shadow: 0 0 transparent;
--color-btn-danger-disabled-text: rgba(248,81,73,0.5);
--color-btn-danger-disabled-bg: #0d1117;
--color-btn-danger-disabled-counter-bg: rgba(218,54,51,0.05);
--color-btn-danger-focus-border: #f85149;
--color-btn-danger-focus-shadow: 0 0 0 3px rgba(248,81,73,0.4);
--color-btn-danger-counter-bg: rgba(218,54,51,0.1);
--color-btn-danger-icon: #f85149;
--color-underlinenav-icon: #484f58;
--color-underlinenav-border-hover: rgba(110,118,129,0.4);
--color-fg-default: #c9d1d9;
--color-fg-muted: #8b949e;
--color-fg-subtle: #484f58;
--color-fg-on-emphasis: #f0f6fc;
--color-canvas-default: #0d1117;
--color-canvas-overlay: #161b22;
--color-canvas-inset: #010409;
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
--color-border-subtle: rgba(240,246,252,0.1);
--color-shadow-small: 0 0 transparent;
--color-shadow-medium: 0 3px 6px #010409;
--color-shadow-large: 0 8px 24px #010409;
--color-shadow-extra-large: 0 12px 48px #010409;
--color-neutral-emphasis-plus: #6e7681;
--color-neutral-emphasis: #6e7681;
--color-neutral-muted: rgba(110,118,129,0.4);
--color-neutral-subtle: rgba(110,118,129,0.1);
--color-accent-fg: #58a6ff;
--color-accent-emphasis: #1f6feb;
--color-accent-muted: rgba(56,139,253,0.4);
--color-accent-subtle: rgba(56,139,253,0.15);
--color-success-fg: #3fb950;
--color-success-emphasis: #238636;
--color-success-muted: rgba(46,160,67,0.4);
--color-success-subtle: rgba(46,160,67,0.15);
--color-attention-fg: #d29922;
--color-attention-emphasis: #9e6a03;
--color-attention-muted: rgba(187,128,9,0.4);
--color-attention-subtle: rgba(187,128,9,0.15);
--color-severe-fg: #db6d28;
--color-severe-emphasis: #bd561d;
--color-severe-muted: rgba(219,109,40,0.4);
--color-severe-subtle: rgba(219,109,40,0.15);
--color-danger-fg: #f85149;
--color-danger-emphasis: #da3633;
--color-danger-muted: rgba(248,81,73,0.4);
--color-danger-subtle: rgba(248,81,73,0.15);
--color-done-fg: #a371f7;
--color-done-emphasis: #8957e5;
--color-done-muted: rgba(163,113,247,0.4);
--color-done-subtle: rgba(163,113,247,0.15);
--color-sponsors-fg: #db61a2;
--color-sponsors-emphasis: #bf4b8a;
--color-sponsors-muted: rgba(219,97,162,0.4);
--color-sponsors-subtle: rgba(219,97,162,0.15);
--color-primer-canvas-backdrop: rgba(1,4,9,0.8);
--color-primer-canvas-sticky: rgba(13,17,23,0.95);
--color-primer-border-active: #F78166;
--color-primer-border-contrast: rgba(240,246,252,0.2);
--color-primer-shadow-highlight: 0 0 transparent;
--color-primer-shadow-inset: 0 0 transparent;
--color-primer-shadow-focus: 0 0 0 3px #0c2d6b;
--color-scale-black: #010409;
--color-scale-white: #f0f6fc;
--color-scale-gray-0: #f0f6fc;
--color-scale-gray-1: #c9d1d9;
--color-scale-gray-2: #b1bac4;
--color-scale-gray-3: #8b949e;
--color-scale-gray-4: #6e7681;
--color-scale-gray-5: #484f58;
--color-scale-gray-6: #30363d;
--color-scale-gray-7: #21262d;
--color-scale-gray-8: #161b22;
--color-scale-gray-9: #0d1117;
--color-scale-blue-0: #cae8ff;
--color-scale-blue-1: #a5d6ff;
--color-scale-blue-2: #79c0ff;
--color-scale-blue-3: #58a6ff;
--color-scale-blue-4: #388bfd;
--color-scale-blue-5: #1f6feb;
--color-scale-blue-6: #1158c7;
--color-scale-blue-7: #0d419d;
--color-scale-blue-8: #0c2d6b;
--color-scale-blue-9: #051d4d;
--color-scale-green-0: #aff5b4;
--color-scale-green-1: #7ee787;
--color-scale-green-2: #56d364;
--color-scale-green-3: #3fb950;
--color-scale-green-4: #2ea043;
--color-scale-green-5: #238636;
--color-scale-green-6: #196c2e;
--color-scale-green-7: #0f5323;
--color-scale-green-8: #033a16;
--color-scale-green-9: #04260f;
--color-scale-yellow-0: #f8e3a1;
--color-scale-yellow-1: #f2cc60;
--color-scale-yellow-2: #e3b341;
--color-scale-yellow-3: #d29922;
--color-scale-yellow-4: #bb8009;
--color-scale-yellow-5: #9e6a03;
--color-scale-yellow-6: #845306;
--color-scale-yellow-7: #693e00;
--color-scale-yellow-8: #4b2900;
--color-scale-yellow-9: #341a00;
--color-scale-orange-0: #ffdfb6;
--color-scale-orange-1: #ffc680;
--color-scale-orange-2: #ffa657;
--color-scale-orange-3: #f0883e;
--color-scale-orange-4: #db6d28;
--color-scale-orange-5: #bd561d;
--color-scale-orange-6: #9b4215;
--color-scale-orange-7: #762d0a;
--color-scale-orange-8: #5a1e02;
--color-scale-orange-9: #3d1300;
--color-scale-red-0: #ffdcd7;
--color-scale-red-1: #ffc1ba;
--color-scale-red-2: #ffa198;
--color-scale-red-3: #ff7b72;
--color-scale-red-4: #f85149;
--color-scale-red-5: #da3633;
--color-scale-red-6: #b62324;
--color-scale-red-7: #8e1519;
--color-scale-red-8: #67060c;
--color-scale-red-9: #490202;
--color-scale-purple-0: #eddeff;
--color-scale-purple-1: #e2c5ff;
--color-scale-purple-2: #d2a8ff;
--color-scale-purple-3: #bc8cff;
--color-scale-purple-4: #a371f7;
--color-scale-purple-5: #8957e5;
--color-scale-purple-6: #6e40c9;
--color-scale-purple-7: #553098;
--color-scale-purple-8: #3c1e70;
--color-scale-purple-9: #271052;
--color-scale-pink-0: #ffdaec;
--color-scale-pink-1: #ffbedd;
--color-scale-pink-2: #ff9bce;
--color-scale-pink-3: #f778ba;
--color-scale-pink-4: #db61a2;
--color-scale-pink-5: #bf4b8a;
--color-scale-pink-6: #9e3670;
--color-scale-pink-7: #7d2457;
--color-scale-pink-8: #5e103e;
--color-scale-pink-9: #42062a;
--color-scale-coral-0: #FFDDD2;
--color-scale-coral-1: #FFC2B2;
--color-scale-coral-2: #FFA28B;
--color-scale-coral-3: #F78166;
--color-scale-coral-4: #EA6045;
--color-scale-coral-5: #CF462D;
--color-scale-coral-6: #AC3220;
--color-scale-coral-7: #872012;
--color-scale-coral-8: #640D04;
--color-scale-coral-9: #460701
}
}

View File

@@ -203,60 +203,4 @@ body {
cursor: pointer;
padding: 0;
font: inherit;
}
/* Auth token section */
.auth-token-section {
margin: 16px 0;
padding: 16px;
background-color: #f6f8fa;
border-radius: 6px;
}
.auth-token-description {
font-size: 12px;
color: #656d76;
margin-bottom: 12px;
}
.auth-token-container {
display: flex;
align-items: center;
gap: 8px;
background-color: #ffffff;
padding: 8px;
}
.auth-token-code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 12px;
color: #1f2328;
border: none;
flex: 1;
padding: 0;
word-break: break-all;
}
.auth-token-refresh {
flex: none;
height: 24px;
width: 24px;
border: none;
outline: none;
color: var(--color-fg-muted);
background: transparent;
padding: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.auth-token-refresh svg {
margin: 0;
}
.auth-token-refresh:not(:disabled):hover {
background-color: var(--color-btn-selected-bg);
}
}

View File

@@ -14,11 +14,9 @@
* limitations under the License.
*/
import React, { useCallback, useEffect, useState } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { createRoot } from 'react-dom/client';
import { Button, TabItem } from './tabItem';
import { AuthTokenSection, getOrCreateAuthToken } from './authToken';
import { Button, TabItem } from './tabItem';
import type { TabInfo } from './tabItem';
type Status =
@@ -39,69 +37,54 @@ const ConnectApp: React.FC = () => {
const [newTab, setNewTab] = useState<boolean>(false);
useEffect(() => {
const runAsync = async () => {
const params = new URLSearchParams(window.location.search);
const relayUrl = params.get('mcpRelayUrl');
const params = new URLSearchParams(window.location.search);
const relayUrl = params.get('mcpRelayUrl');
if (!relayUrl) {
setShowButtons(false);
setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' });
return;
}
if (!relayUrl) {
setShowButtons(false);
setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' });
return;
}
setMcpRelayUrl(relayUrl);
setMcpRelayUrl(relayUrl);
try {
const client = JSON.parse(params.get('client') || '{}');
const info = `${client.name}/${client.version}`;
setClientInfo(info);
setStatus({
type: 'connecting',
message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?`
});
} catch (e) {
setStatus({ type: 'error', message: 'Failed to parse client version.' });
return;
}
try {
const client = JSON.parse(params.get('client') || '{}');
const info = `${client.name}/${client.version}`;
setClientInfo(info);
setStatus({
type: 'connecting',
message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?`
});
} catch (e) {
setStatus({ type: 'error', message: 'Failed to parse client version.' });
return;
}
const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10);
const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion;
if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) {
const extensionVersion = chrome.runtime.getManifest().version;
setShowButtons(false);
setShowTabList(false);
setStatus({
type: 'error',
versionMismatch: {
extensionVersion,
}
});
return;
}
const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10);
const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion;
if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) {
const extensionVersion = chrome.runtime.getManifest().version;
setShowButtons(false);
setShowTabList(false);
setStatus({
type: 'error',
versionMismatch: {
extensionVersion,
}
});
return;
}
const expectedToken = getOrCreateAuthToken();
const token = params.get('token');
if (token === expectedToken) {
await connectToMCPRelay(relayUrl);
await handleConnectToTab();
return;
}
if (token) {
handleReject('Invalid token provided.');
return;
}
void connectToMCPRelay(relayUrl);
await connectToMCPRelay(relayUrl);
// If this is a browser_navigate command, hide the tab list and show simple allow/reject
if (params.get('newTab') === 'true') {
setNewTab(true);
setShowTabList(false);
} else {
await loadTabs();
}
};
void runAsync();
// If this is a browser_navigate command, hide the tab list and show simple allow/reject
if (params.get('newTab') === 'true') {
setNewTab(true);
setShowTabList(false);
} else {
void loadTabs();
}
}, []);
const handleReject = useCallback((message: string) => {
@@ -111,6 +94,7 @@ const ConnectApp: React.FC = () => {
}, []);
const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => {
const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl });
if (!response.success)
handleReject(response.error);
@@ -190,10 +174,6 @@ const ConnectApp: React.FC = () => {
</div>
)}
{status?.type === 'connecting' && (
<AuthTokenSection />
)}
{showTabList && (
<div>
<div className='tab-section-title'>

View File

@@ -1,39 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.copy-icon {
flex: none;
height: 24px;
width: 24px;
border: none;
outline: none;
color: var(--color-fg-muted);
background: transparent;
padding: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.copy-icon svg {
margin: 0;
}
.copy-icon:not(:disabled):hover {
background-color: var(--color-btn-selected-bg);
}

View File

@@ -1,54 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import * as icons from './icons';
import './copyToClipboard.css';
type CopyToClipboardProps = {
value: string;
};
/**
* A copy to clipboard button.
*/
export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({ value }) => {
type IconType = 'copy' | 'check' | 'cross';
const [icon, setIcon] = React.useState<IconType>('copy');
React.useEffect(() => {
setIcon('copy');
}, [value]);
React.useEffect(() => {
if (icon === 'check') {
const timeout = setTimeout(() => {
setIcon('copy');
}, 3000);
return () => clearTimeout(timeout);
}
}, [icon]);
const handleCopy = React.useCallback(() => {
navigator.clipboard.writeText(value).then(() => {
setIcon('check');
}, () => {
setIcon('cross');
});
}, [value]);
const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();
return <button className='copy-icon' title='Copy to clipboard' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
};

View File

@@ -1,32 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.octicon {
display: inline-block;
overflow: visible !important;
vertical-align: text-bottom;
fill: currentColor;
margin-right: 7px;
flex: none;
}
.color-icon-success {
color: var(--color-success-fg) !important;
}
.color-text-danger {
color: var(--color-danger-fg) !important;
}

View File

@@ -1,49 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import './icons.css';
import './colors.css';
export const cross = () => {
return <svg className='octicon color-text-danger' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'>
<path fillRule='evenodd' d='M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z'></path>
</svg>;
};
export const check = () => {
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-icon-success'>
<path fillRule='evenodd' d='M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z'></path>
</svg>;
};
export const copy = () => {
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16' aria-hidden='true'>
<path d='M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z'></path>
<path d='M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z'></path>
</svg>;
};
export const refresh = () => {
return <svg className='octicon' viewBox="0 0 16 16" width="16" height="16" aria-hidden='true'>
<path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"></path>
</svg>;
};
export const chevronDown = () => {
return <svg className='octicon' viewBox="0 0 16 16" width="16" height="16" aria-hidden='true'>
<path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z"></path>
</svg>;
};

View File

@@ -19,7 +19,6 @@ import { createRoot } from 'react-dom/client';
import { Button, TabItem } from './tabItem';
import type { TabInfo } from './tabItem';
import { AuthTokenSection } from './authToken';
interface ConnectionStatus {
isConnected: boolean;
@@ -98,7 +97,6 @@ const StatusApp: React.FC = () => {
No MCP clients are currently connected.
</div>
)}
<AuthTokenSection />
</div>
</div>
);

View File

@@ -304,33 +304,3 @@ test(`custom executablePath`, async ({ startClient, server, useShortConnectionTi
});
expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?');
});
test(`bypass connection dialog with token`, async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch();
const page = await browserContext.newPage();
await page.goto('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/status.html');
const token = await page.locator('.auth-token-code').textContent();
const [name, value] = token?.split('=') || [];
const { client } = await startClient({
args: [`--extension`],
extensionToken: value,
config: {
browser: {
userDataDir: browserWithExtension.userDataDir,
}
},
});
const navigateResponse = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(await navigateResponse).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
});

34
package-lock.json generated
View File

@@ -1,23 +1,23 @@
{
"name": "@playwright/mcp",
"version": "0.0.41",
"version": "0.0.37",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@playwright/mcp",
"version": "0.0.41",
"version": "0.0.37",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.56.0-alpha-2025-10-01",
"playwright-core": "1.56.0-alpha-2025-10-01"
"playwright": "1.56.0-alpha-2025-09-06",
"playwright-core": "1.56.0-alpha-2025-09-06"
},
"bin": {
"mcp-server-playwright": "cli.js"
},
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.17.5",
"@playwright/test": "1.56.0-alpha-2025-10-01",
"@playwright/test": "1.56.0-alpha-2025-09-06",
"@types/node": "^24.3.0",
"zod-to-json-schema": "^3.24.6"
},
@@ -50,13 +50,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.56.0-alpha-2025-10-01",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-10-01.tgz",
"integrity": "sha512-1Sq2gaxXdrbx1fp5lHhOK6HHT29Iov5AjH7h2Mn223QPDpZ+7o9Jr1lz5An3R4tqAuKZkf543fFfaR6wjD0Osw==",
"version": "1.56.0-alpha-2025-09-06",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-06.tgz",
"integrity": "sha512-BhjZwYn26kGwWVoF9et+4RnSb/HuJ2cbzDPACWdkBApEb+2fBb060cOQZ63hZ+IIeQUd2Q4TCZbH5U9tvXD23A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.56.0-alpha-2025-10-01"
"playwright": "1.56.0-alpha-2025-09-06"
},
"bin": {
"playwright": "cli.js"
@@ -379,7 +379,6 @@
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
@@ -826,12 +825,12 @@
}
},
"node_modules/playwright": {
"version": "1.56.0-alpha-2025-10-01",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-10-01.tgz",
"integrity": "sha512-fB26N0+NZyITpUIKBlJjA3CoFqBfNlhns/6urmK50nY6DgaHptCfKSJJLDhd37nsjQJ1ysV4n6WXSL8ifaEK6g==",
"version": "1.56.0-alpha-2025-09-06",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-06.tgz",
"integrity": "sha512-suVjiF5eeUtIqFq5E/5LGgkV0/bRSik87N+M7uLsjPQrKln9QHbZt3cy7Zybicj3ZqTBWWHvpN9b4cnpg6hS0g==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.56.0-alpha-2025-10-01"
"playwright-core": "1.56.0-alpha-2025-09-06"
},
"bin": {
"playwright": "cli.js"
@@ -844,9 +843,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.56.0-alpha-2025-10-01",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-10-01.tgz",
"integrity": "sha512-/n6Wa2cksUAj+thJhZpR1PvnK0E3WHhNBMTxr1G6XgdhhvUdX7y4Y6aU9FaJPa5S0P8tcX3jyBXLCYIv/pYlJA==",
"version": "1.56.0-alpha-2025-09-06",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-06.tgz",
"integrity": "sha512-B2s/cuqYuu+mT4hIHG8gIOXjCSKh0Np1gJNCp0CrDk/UTLB74gThwXiyPAJU0fADIQH6Dv1glv8ZvKTDVT8Fng==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@@ -1229,7 +1228,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp",
"version": "0.0.41",
"version": "0.0.37",
"description": "Playwright Tools for MCP",
"repository": {
"type": "git",
@@ -22,10 +22,7 @@
"ctest": "playwright test --project=chrome",
"ftest": "playwright test --project=firefox",
"wtest": "playwright test --project=webkit",
"dtest": "MCP_IN_DOCKER=1 playwright test --project=chromium-docker",
"npm-publish": "npm run clean && npm run test && npm publish",
"copy-config": "cp ../playwright/packages/playwright/src/mcp/config.d.ts . && perl -pi -e \"s|import type \\* as playwright from 'playwright-core';|import type * as playwright from 'playwright';|\" ./config.d.ts",
"roll": "npm run copy-config && npm run lint"
"dtest": "MCP_IN_DOCKER=1 playwright test --project=chromium-docker"
},
"exports": {
"./package.json": "./package.json",
@@ -35,15 +32,15 @@
}
},
"dependencies": {
"playwright": "1.56.0-alpha-2025-10-01",
"playwright-core": "1.56.0-alpha-2025-10-01"
"playwright": "1.56.0-alpha-2025-09-06",
"playwright-core": "1.56.0-alpha-2025-09-06"
},
"bin": {
"mcp-server-playwright": "cli.js"
},
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.17.5",
"@playwright/test": "1.56.0-alpha-2025-10-01",
"@playwright/test": "1.56.0-alpha-2025-09-06",
"@types/node": "^24.3.0",
"zod-to-json-schema": "^3.24.6"
}

View File

@@ -46,7 +46,6 @@ export type StartClient = (options?: {
config?: Config,
roots?: { name: string, uri: string }[],
rootsResponseDelay?: number,
extensionToken?: string,
}) => Promise<{ client: Client, stderr: () => string }>;
@@ -103,7 +102,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
};
});
}
const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken);
const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright'));
let stderrBuffer = '';
stderr?.on('data', data => {
if (process.env.PWMCP_DEBUG)
@@ -182,7 +181,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
},
});
async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{
async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string): Promise<{
transport: Transport,
stderr: Stream | null,
}> {
@@ -209,7 +208,6 @@ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'],
DEBUG_COLORS: '0',
DEBUG_HIDE_DATE: '1',
PWMCP_PROFILES_DIR_FOR_TEST: profilesDir,
...(extensionToken ? { PLAYWRIGHT_MCP_EXTENSION_TOKEN: extensionToken } : {}),
},
});
return {

View File

@@ -21,7 +21,7 @@ const path = require('path')
const { zodToJsonSchema } = require('zod-to-json-schema')
const { execSync } = require('child_process');
const { browserTools } = require('playwright/lib/mcp/browser/tools');
const { allTools } = require('playwright/lib/mcp/browser/tools');
const capabilities = {
'core': 'Core automation',
@@ -33,7 +33,7 @@ const capabilities = {
'tracing': 'Tracing (opt-in via --caps=tracing)',
};
const toolsByCapability = Object.fromEntries(Object.entries(capabilities).map(([capability, title]) => [title, browserTools.filter(tool => tool.capability === capability).sort((a, b) => a.schema.name.localeCompare(b.schema.name))]));
const toolsByCapability = Object.fromEntries(Object.entries(capabilities).map(([capability, title]) => [title, allTools.filter(tool => tool.capability === capability).sort((a, b) => a.schema.name.localeCompare(b.schema.name))]));
/**
* @param {any} tool