diff --git a/extension/src/ui/authToken.css b/extension/src/ui/authToken.css new file mode 100644 index 0000000..d44eb76 --- /dev/null +++ b/extension/src/ui/authToken.css @@ -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); +} diff --git a/extension/src/ui/authToken.tsx b/extension/src/ui/authToken.tsx new file mode 100644 index 0000000..7da55fb --- /dev/null +++ b/extension/src/ui/authToken.tsx @@ -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(getOrCreateAuthToken); + + const onRegenerateToken = useCallback(() => { + const newToken = generateAuthToken(); + localStorage.setItem('auth-token', newToken); + setAuthToken(newToken); + }, []); + + return ( +
+
+ Set this environment variable to bypass the connection dialog: +
+
+ 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; +} diff --git a/extension/src/ui/colors.css b/extension/src/ui/colors.css new file mode 100644 index 0000000..42b254f --- /dev/null +++ b/extension/src/ui/colors.css @@ -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 + } +} diff --git a/extension/src/ui/connect.css b/extension/src/ui/connect.css index 60945d4..fe2588f 100644 --- a/extension/src/ui/connect.css +++ b/extension/src/ui/connect.css @@ -203,4 +203,60 @@ body { cursor: pointer; padding: 0; font: inherit; -} \ No newline at end of file +} + +/* 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); +} diff --git a/extension/src/ui/connect.tsx b/extension/src/ui/connect.tsx index 153017b..e9e410e 100644 --- a/extension/src/ui/connect.tsx +++ b/extension/src/ui/connect.tsx @@ -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(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 = () => { )} + {status?.type === 'connecting' && ( + + )} + {showTabList && (
diff --git a/extension/src/ui/copyToClipboard.css b/extension/src/ui/copyToClipboard.css new file mode 100644 index 0000000..d929947 --- /dev/null +++ b/extension/src/ui/copyToClipboard.css @@ -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); +} diff --git a/extension/src/ui/copyToClipboard.tsx b/extension/src/ui/copyToClipboard.tsx new file mode 100644 index 0000000..487eaba --- /dev/null +++ b/extension/src/ui/copyToClipboard.tsx @@ -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 = ({ value }) => { + type IconType = 'copy' | 'check' | 'cross'; + const [icon, setIcon] = React.useState('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 ; +}; diff --git a/extension/src/ui/icons.css b/extension/src/ui/icons.css new file mode 100644 index 0000000..8abcf98 --- /dev/null +++ b/extension/src/ui/icons.css @@ -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; +} diff --git a/extension/src/ui/icons.tsx b/extension/src/ui/icons.tsx new file mode 100644 index 0000000..04bd17d --- /dev/null +++ b/extension/src/ui/icons.tsx @@ -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 ; +}; + +export const check = () => { + return ; +}; + +export const copy = () => { + return ; +}; + +export const refresh = () => { + return ; +}; diff --git a/extension/src/ui/status.tsx b/extension/src/ui/status.tsx index f8de788..ca2e1ef 100644 --- a/extension/src/ui/status.tsx +++ b/extension/src/ui/status.tsx @@ -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.
)} +
); diff --git a/extension/tests/extension.spec.ts b/extension/tests/extension.spec.ts index 91dbed1..291390e 100644 --- a/extension/tests/extension.spec.ts +++ b/extension/tests/extension.spec.ts @@ -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!`), + }); + + +}); diff --git a/tests/fixtures.ts b/tests/fixtures.ts index 80d03b9..d8496e4 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -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( }; }); } - 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( }, }); -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 {