feat(extension): bypass connection dialog when token is provided (#1024)

<img width="698" height="551" alt="image"
src="https://github.com/user-attachments/assets/feee2375-7654-42cb-b9fa-f48aee5dd045"
/>


Reference: https://github.com/microsoft/playwright-mcp/issues/965
This commit is contained in:
Yury Semikhatsky
2025-09-10 15:33:31 -07:00
committed by GitHub
parent dc3419023f
commit 45fee738bc
12 changed files with 1355 additions and 48 deletions

View File

@@ -0,0 +1,70 @@
/*
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);
}

View File

@@ -0,0 +1,68 @@
/**
* 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 onRegenerateToken = useCallback(() => {
const newToken = generateAuthToken();
localStorage.setItem('auth-token', newToken);
setAuthToken(newToken);
}, []);
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'>PLAYWRIGHT_MCP_EXTENSION_TOKEN={authToken}</code>
<button className='auth-token-refresh' title='Generate new token' aria-label='Generate new token'onClick={onRegenerateToken}>{icons.refresh()}</button>
<CopyToClipboard value={authToken} />
</div>
</div>
);
};
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;
}

891
extension/src/ui/colors.css Normal file
View File

@@ -0,0 +1,891 @@
/* 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,4 +203,60 @@ 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,9 +14,11 @@
* limitations under the License.
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Button, TabItem } from './tabItem';
import { Button, TabItem } from './tabItem';
import { AuthTokenSection, getOrCreateAuthToken } from './authToken';
import type { TabInfo } from './tabItem';
type Status =
@@ -37,54 +39,69 @@ const ConnectApp: React.FC = () => {
const [newTab, setNewTab] = useState<boolean>(false);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const relayUrl = params.get('mcpRelayUrl');
const runAsync = async () => {
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;
}
void connectToMCPRelay(relayUrl);
const expectedToken = getOrCreateAuthToken();
const token = params.get('token');
if (token === expectedToken) {
await connectToMCPRelay(relayUrl);
await handleConnectToTab();
return;
}
if (token) {
handleReject('Invalid token provided.');
return;
}
// 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();
}
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();
}, []);
const handleReject = useCallback((message: string) => {
@@ -94,7 +111,6 @@ 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);
@@ -174,6 +190,10 @@ const ConnectApp: React.FC = () => {
</div>
)}
{status?.type === 'connecting' && (
<AuthTokenSection />
)}
{showTabList && (
<div>
<div className='tab-section-title'>

View File

@@ -0,0 +1,39 @@
/*
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

@@ -0,0 +1,54 @@
/**
* 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

@@ -0,0 +1,32 @@
/*
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

@@ -0,0 +1,43 @@
/*
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>;
};

View File

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

View File

@@ -304,3 +304,33 @@ 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!`),
});
});

View File

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