new mcp code. tests kind of work

This commit is contained in:
Tommy D. Rossi
2025-11-15 22:30:08 +01:00
parent f26178c51a
commit 5c9148e8e1
4 changed files with 748 additions and 1370 deletions
+136 -207
View File
@@ -1,229 +1,158 @@
import { createMCPClient } from './mcp-client.js'
import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
const execAsync = promisify(exec)
function js(strings: TemplateStringsArray, ...values: any[]): string {
return strings.reduce(
(result, str, i) => result + str + (values[i] || ''),
'',
)
}
async function killProcessOnPort(port: number): Promise<void> {
try {
const { stdout } = await execAsync(`lsof -ti:${port}`)
const pid = stdout.trim()
if (pid) {
await execAsync(`kill -9 ${pid}`)
console.log(`Killed process ${pid} on port ${port}`)
await new Promise((resolve) => setTimeout(resolve, 500))
}
} catch (error) {
// No process running on port or already killed
}
}
describe('MCP Server Tests', () => {
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
let cleanup: (() => Promise<void>) | null = null
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
let cleanup: (() => Promise<void>) | null = null
beforeAll(async () => {
const result = await createMCPClient()
client = result.client
cleanup = result.cleanup
})
afterAll(async () => {
if (cleanup) {
await cleanup()
cleanup = null
}
})
it('should capture console logs', async () => {
// Connect first (open a new page)
const connectResult = await client.callTool({
name: 'new_page',
arguments: {},
beforeAll(async () => {
await killProcessOnPort(9988)
const result = await createMCPClient()
client = result.client
cleanup = result.cleanup
})
expect(connectResult.content).toBeDefined()
expect(connectResult.content).toMatchInlineSnapshot(`
afterAll(async () => {
if (cleanup) {
await cleanup()
cleanup = null
}
})
it('should execute code and capture console output', async () => {
await client.callTool({
name: 'execute',
arguments: {
code: js`
const newPage = await context.newPage();
state.page = newPage;
`,
},
})
const result = await client.callTool({
name: 'execute',
arguments: {
code: js`
await state.page.goto('https://news.ycombinator.com');
const title = await state.page.title();
console.log('Page title:', title);
return { url: state.page.url(), title };
`,
},
})
expect(result.content).toMatchInlineSnapshot(`
[
{
"text": "Created new page. URL: about:blank. Total pages: 20",
"text": "Console output:
[log] Page title: Hacker News
Return value:
{
"url": "https://news.ycombinator.com/",
"title": "Hacker News"
}",
"type": "text",
},
]
`)
expect(result.content).toBeDefined()
}, 30000)
// Navigate to a page and log something
const result = await client.callTool({
name: 'execute',
arguments: {
code: `
await page.goto('https://news.ycombinator.com');
await page.evaluate(() => {
console.log('Test log message');
console.error('Test error message');
});
`,
},
})
expect(result.content).toBeDefined()
expect(result.content).toMatchInlineSnapshot(`
[
{
"text": "Code executed successfully (no output)",
"type": "text",
it('should get accessibility snapshot of hacker news', async () => {
await client.callTool({
name: 'execute',
arguments: {
code: js`
const newPage = await context.newPage();
state.page = newPage;
`,
},
]
`)
})
// Get console logs
const logsResult = (await client.callTool({
name: 'console_logs',
arguments: {
limit: 10,
},
})) as CallToolResult
expect(logsResult.content).toBeDefined()
expect(logsResult.content).toMatchInlineSnapshot(`
[
{
"text": "[log]: Test log message :1:32
[error]: Test error message :2:32",
"type": "text",
const result = await client.callTool({
name: 'execute',
arguments: {
code: js`
await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' });
const snapshot = await state.page._snapshotForAI();
return snapshot;
`,
},
]
`)
})
// Close the page opened
await client.callTool({
name: 'close_page',
arguments: {},
})
}, 30000)
const initialData =
typeof result === 'object' && result.content?.[0]?.text
? tryJsonParse(result.content[0].text)
: result
expect(initialData).toMatchFileSnapshot(
'snapshots/hacker-news-initial-accessibility.md',
)
expect(result.content).toBeDefined()
expect(initialData).toContain('table')
expect(initialData).toContain('Hacker News')
}, 30000)
it('should capture accessibility snapshot of hacker news', async () => {
// Create new page
await client.callTool({
name: 'new_page',
arguments: {},
})
it('should get accessibility snapshot of shadcn UI', async () => {
await client.callTool({
name: 'execute',
arguments: {
code: js`
const newPage = await context.newPage();
state.page = newPage;
`,
},
})
// Navigate to a specific old Hacker News story that won't change
await client.callTool({
name: 'execute',
arguments: {
code: `await page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' })`,
},
})
const snapshot = await client.callTool({
name: 'execute',
arguments: {
code: js`
await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' });
const snapshot = await state.page._snapshotForAI();
return snapshot;
`,
},
})
// Get initial accessibility snapshot
const initialSnapshot = await client.callTool({
name: 'accessibility_snapshot',
arguments: {},
})
expect(initialSnapshot.content).toBeDefined()
// Save initial snapshot
const initialData =
typeof initialSnapshot === 'object' && initialSnapshot.content?.[0]?.text
? tryJsonParse(initialSnapshot.content[0].text)
: initialSnapshot
expect(initialData).toMatchFileSnapshot('snapshots/hacker-news-initial-accessibility.md')
expect(initialData).toContain('table')
expect(initialData).toContain('Hacker News')
// Focus on first link on the page
await client.callTool({
name: 'execute',
arguments: {
code: `
// Find and focus the first link
const firstLink = await page.$('a')
if (firstLink) {
await firstLink.focus()
const linkText = await firstLink.textContent()
console.log('Focused on first link:', linkText)
}
`,
},
})
// Get snapshot after focusing
const focusedSnapshot = await client.callTool({
name: 'accessibility_snapshot',
arguments: {},
})
expect(focusedSnapshot.content).toBeDefined()
// Save focused snapshot
const focusedData =
typeof focusedSnapshot === 'object' && focusedSnapshot.content?.[0]?.text
? tryJsonParse(focusedSnapshot.content[0].text)
: focusedSnapshot
expect(focusedData).toMatchFileSnapshot('snapshots/hacker-news-focused-accessibility.md')
// Verify the snapshot contains expected content
expect(focusedData).toBeDefined()
expect(focusedData).toContain('link')
// Press Tab to go to next item
await client.callTool({
name: 'execute',
arguments: {
code: `
await page.keyboard.press('Tab')
console.log('Pressed Tab key')
`,
},
})
// Get snapshot after tab navigation
const tabbedSnapshot = await client.callTool({
name: 'accessibility_snapshot',
arguments: {},
})
expect(tabbedSnapshot.content).toBeDefined()
// Save tabbed snapshot
const tabbedData =
typeof tabbedSnapshot === 'object' && tabbedSnapshot.content?.[0]?.text
? tryJsonParse(tabbedSnapshot.content[0].text)
: tabbedSnapshot
expect(tabbedData).toMatchFileSnapshot('snapshots/hacker-news-tabbed-accessibility.md')
// Verify the snapshot is different
expect(tabbedData).toBeDefined()
expect(tabbedData).toContain('Hacker News')
// Close the page opened
await client.callTool({
name: 'close_page',
arguments: {},
})
}, 30000)
it('should capture accessibility snapshot of shadcn UI', async () => {
// Create new page
await client.callTool({
name: 'new_page',
arguments: {},
})
// Navigate to shadcn UI
await client.callTool({
name: 'execute',
arguments: {
code: `await page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' })`,
},
})
// Get accessibility snapshot
const snapshot = await client.callTool({
name: 'accessibility_snapshot',
arguments: {},
})
expect(snapshot.content).toBeDefined()
// Save snapshot
const data =
typeof snapshot === 'object' && snapshot.content?.[0]?.text ? tryJsonParse(snapshot.content[0].text) : snapshot
expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md')
expect(data).toContain('shadcn')
// Close the page opened
await client.callTool({
name: 'close_page',
arguments: {},
})
}, 30000)
const data =
typeof snapshot === 'object' && snapshot.content?.[0]?.text
? tryJsonParse(snapshot.content[0].text)
: snapshot
expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md')
expect(snapshot.content).toBeDefined()
expect(data).toContain('shadcn')
}, 30000)
})
function tryJsonParse(str: string) {
try {
return JSON.parse(str)
} catch {
return str
}
try {
return JSON.parse(str)
} catch {
return str
}
}
+66 -523
View File
@@ -1,239 +1,78 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import { Page, Browser, BrowserContext, chromium } from 'playwright-core'
import { Page, Browser, chromium } from 'playwright-core'
import fs from 'node:fs'
import path from 'node:path'
import os from 'node:os'
import { spawn } from 'child_process'
import type { ChildProcess } from 'child_process'
import { getBrowserExecutablePath } from './browser-config.js'
import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
import vm from 'node:vm'
// Chrome executable finding logic moved to browser-config.ts
const require = createRequire(import.meta.url)
// Store for maintaining state across tool calls
interface ToolState {
isConnected: boolean
page: Page | null
browser: Browser | null
chromeProcess: ChildProcess | null
consoleLogs: Map<Page, ConsoleMessage[]>
networkRequests: Map<Page, NetworkRequest[]>
}
const state: ToolState = {
isConnected: false,
page: null,
browser: null,
chromeProcess: null,
consoleLogs: new Map(),
networkRequests: new Map(),
}
interface ConsoleMessage {
type: string
text: string
timestamp: number
location?: {
url: string
lineNumber: number
columnNumber: number
}
}
const RELAY_PORT = 9988
interface NetworkRequest {
url: string
method: string
status: number
headers: Record<string, string>
timestamp: number
duration: number
size: number
requestBody?: any
responseBody?: any
}
const CDP_PORT = 9922
// Check if CDP is available on the specified port
async function isCDPAvailable(): Promise<boolean> {
async function isPortTaken(port: number): Promise<boolean> {
try {
const response = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`)
const response = await fetch(`http://localhost:${port}/`)
return response.ok
} catch {
return false
}
}
// Launch Chrome with CDP enabled
async function launchChromeWithCDP(): Promise<ChildProcess> {
const userDataDir = path.join(os.homedir(), '.playwriter')
if (!fs.existsSync(userDataDir)) {
fs.mkdirSync(userDataDir, { recursive: true })
async function ensureRelayServer(): Promise<void> {
const portTaken = await isPortTaken(RELAY_PORT)
if (portTaken) {
console.error('CDP relay server already running')
return
}
const executablePath = getBrowserExecutablePath()
console.error('Starting CDP relay server...')
const chromeArgs = [
`--remote-debugging-port=${CDP_PORT}`,
`--user-data-dir=${userDataDir}`,
'--no-first-run',
'--no-default-browser-check',
'--disable-session-crashed-bubble',
'--disable-features=DevToolsDebuggingRestrictions',
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-web-security',
'--disable-infobars',
'--disable-translate',
'--disable-features=AutomationControlled', // disables --enable-automation
'--disable-background-timer-throttling',
'--disable-popup-blocking',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-window-activation',
'--disable-focus-on-load',
'--no-startup-window',
'--window-position=0,0',
'--disable-site-isolation-trials',
'--disable-features=IsolateOrigins,site-per-process',
]
const scriptPath = require.resolve('../dist/start-relay-server.js')
const chromeProcess = spawn(executablePath, chromeArgs, {
const serverProcess = spawn(process.execPath, [scriptPath], {
detached: true,
stdio: 'ignore',
})
// Unref the process so it doesn't keep the parent process alive
chromeProcess.unref()
serverProcess.unref()
// Give Chrome time to start up
await new Promise((resolve) => setTimeout(resolve, 2000))
// wait for extension to connect
await new Promise((resolve) => setTimeout(resolve, 1000))
return chromeProcess
console.error('CDP relay server started')
}
// Ensure connection to Chrome via CDP
async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
if (state.isConnected && state.browser && state.page) {
return { browser: state.browser, page: state.page }
}
// Check if CDP is already available
const cdpAvailable = await isCDPAvailable()
await ensureRelayServer()
if (!cdpAvailable) {
// Launch Chrome with CDP
const chromeProcess = await launchChromeWithCDP()
state.chromeProcess = chromeProcess
}
const cdpEndpoint = `ws://localhost:${RELAY_PORT}/cdp/${Date.now()}`
const browser = await chromium.connectOverCDP(cdpEndpoint)
// Connect to Chrome via CDP
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${CDP_PORT}`)
// Get the default context
const contexts = browser.contexts()
let context: BrowserContext
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
if (contexts.length > 0) {
context = contexts[0]
} else {
context = await browser.newContext()
}
// Generate user agent and set it on context
const { default: UserAgent } = await import('user-agents')
const userAgent = new UserAgent({
platform: 'MacIntel',
deviceCategory: 'desktop',
})
// Get or create page
const pages = context.pages()
let page: Page
if (pages.length > 0) {
page = pages[0]
// Set user agent on existing page
await page.setExtraHTTPHeaders({
'User-Agent': userAgent.toString(),
})
} else {
page = await context.newPage()
// Set user agent on new page
await page.setExtraHTTPHeaders({
'User-Agent': userAgent.toString(),
})
}
// Set up event listeners if not already set
if (!state.isConnected) {
page.on('console', (msg) => {
// Get or create logs array for this page
let pageLogs = state.consoleLogs.get(page)
if (!pageLogs) {
pageLogs = []
state.consoleLogs.set(page, pageLogs)
}
// Add new log
pageLogs.push({
type: msg.type(),
text: msg.text(),
timestamp: Date.now(),
location: msg.location(),
})
// Keep only last 1000 logs
if (pageLogs.length > 1000) {
pageLogs.shift()
}
})
// Clean up logs and network requests when page is closed
page.on('close', () => {
state.consoleLogs.delete(page)
state.networkRequests.delete(page)
})
page.on('request', (request) => {
const startTime = Date.now()
const entry: Partial<NetworkRequest> = {
url: request.url(),
method: request.method(),
headers: request.headers(),
timestamp: startTime,
}
request
.response()
.then((response) => {
if (response) {
entry.status = response.status()
entry.duration = Date.now() - startTime
entry.size = response.headers()['content-length'] ? parseInt(response.headers()['content-length']) : 0
// Get or create requests array for this page
let pageRequests = state.networkRequests.get(page)
if (!pageRequests) {
pageRequests = []
state.networkRequests.set(page, pageRequests)
}
// Add new request
pageRequests.push(entry as NetworkRequest)
// Keep only last 1000 requests
if (pageRequests.length > 1000) {
pageRequests.shift()
}
}
})
.catch(() => {
// Handle response errors silently
})
})
}
const page = pages.length > 0 ? pages[0] : await context.newPage()
state.browser = browser
state.page = page
@@ -242,319 +81,30 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
return { browser, page }
}
// Initialize MCP server
function getCurrentPage(): Page {
if (state.page) {
return state.page
}
if (state.browser) {
const contexts = state.browser.contexts()
if (contexts.length > 0) {
const pages = contexts[0].pages()
if (pages.length > 0) {
return pages[0]
}
}
}
throw new Error('No page available')
}
const server = new McpServer({
name: 'playwriter',
title: 'Playwright MCP Server',
version: '1.0.0',
})
// Tool 1: New Page - Creates a new browser page
server.tool('new_page', 'Create a new browser page in the shared Chrome instance', {}, async () => {
try {
const { browser, page } = await ensureConnection()
// Always create a new page
const context = browser.contexts()[0] || (await browser.newContext())
const newPage = await context.newPage()
// Set user agent on new page
const { default: UserAgent } = await import('user-agents')
const userAgent = new UserAgent({
platform: 'MacIntel',
deviceCategory: 'desktop',
})
await newPage.setExtraHTTPHeaders({
'User-Agent': userAgent.toString(),
})
// Update state to use the new page
state.page = newPage
// Set up event listeners on the new page
newPage.on('console', (msg) => {
// Get or create logs array for this page
let pageLogs = state.consoleLogs.get(newPage)
if (!pageLogs) {
pageLogs = []
state.consoleLogs.set(newPage, pageLogs)
}
// Add new log
pageLogs.push({
type: msg.type(),
text: msg.text(),
timestamp: Date.now(),
location: msg.location(),
})
// Keep only last 1000 logs
if (pageLogs.length > 1000) {
pageLogs.shift()
}
})
// Clean up logs and network requests when page is closed
newPage.on('close', () => {
state.consoleLogs.delete(newPage)
state.networkRequests.delete(newPage)
})
newPage.on('request', (request) => {
const startTime = Date.now()
const entry: Partial<NetworkRequest> = {
url: request.url(),
method: request.method(),
headers: request.headers(),
timestamp: startTime,
}
request
.response()
.then((response) => {
if (response) {
entry.status = response.status()
entry.duration = Date.now() - startTime
entry.size = response.headers()['content-length'] ? parseInt(response.headers()['content-length']) : 0
// Get or create requests array for this page
let pageRequests = state.networkRequests.get(newPage)
if (!pageRequests) {
pageRequests = []
state.networkRequests.set(newPage, pageRequests)
}
// Add new request
pageRequests.push(entry as NetworkRequest)
// Keep only last 1000 requests
if (pageRequests.length > 1000) {
pageRequests.shift()
}
}
})
.catch(() => {
// Handle response errors silently
})
})
return {
content: [
{
type: 'text',
text: `Created new page. URL: ${newPage.url()}. Total pages: ${context.pages().length}`,
},
],
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to create new page: ${error.message}`,
},
],
isError: true,
}
}
})
// Tool 2: Console Logs
server.tool(
'console_logs',
'Retrieve console messages from the page',
{
limit: z.number().default(50).describe('Maximum number of messages to return'),
type: z.enum(['log', 'info', 'warning', 'error', 'debug']).optional().describe('Filter by message type'),
offset: z.number().default(0).describe('Start from this index'),
},
async ({ limit, type, offset }) => {
try {
const { page } = await ensureConnection() // Ensure we're connected first
// Get logs for current page
const pageLogs = state.consoleLogs.get(page) || []
// Filter and paginate logs
let logs = [...pageLogs]
if (type) {
logs = logs.filter((log) => log.type === type)
}
const paginatedLogs = logs.slice(offset, offset + limit)
// Format logs to look like Chrome console output
let consoleOutput = ''
if (paginatedLogs.length === 0) {
consoleOutput = 'No console messages'
} else {
consoleOutput = paginatedLogs
.map((log) => {
const timestamp = new Date(log.timestamp).toLocaleTimeString()
const location = log.location
? ` ${log.location.url}:${log.location.lineNumber}:${log.location.columnNumber}`
: ''
return `[${log.type}]: ${log.text}${location}`
})
.join('\n')
if (logs.length > paginatedLogs.length) {
consoleOutput += `\n\n(Showing ${paginatedLogs.length} of ${logs.length} total messages)`
}
}
return {
content: [
{
type: 'text',
text: consoleOutput,
},
],
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to get console logs: ${error.message}`,
},
],
isError: true,
}
}
},
)
// Tool 3: Network History
server.tool(
'network_history',
'Get history of network requests',
{
limit: z.number().default(50).describe('Maximum number of requests to return'),
urlPattern: z.string().optional().describe('Filter by URL pattern (supports wildcards)'),
method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('Filter by HTTP method'),
statusCode: z
.object({
min: z.number().optional(),
max: z.number().optional(),
})
.optional()
.describe('Filter by status code range'),
includeBody: z.boolean().default(false).describe('Include request/response bodies'),
},
async ({ limit, urlPattern, method, statusCode, includeBody }) => {
try {
const { page } = await ensureConnection()
// Get requests for current page
const pageRequests = state.networkRequests.get(page) || []
// If includeBody is requested, we need to fetch bodies for existing requests
if (includeBody && pageRequests.length > 0) {
// Note: In a real implementation, you'd store bodies during capture
console.warn('Body capture not implemented in this example')
}
// Filter requests
let requests = [...pageRequests]
if (urlPattern) {
const pattern = new RegExp(urlPattern.replace(/\*/g, '.*'))
requests = requests.filter((req) => pattern.test(req.url))
}
if (method) {
requests = requests.filter((req) => req.method === method)
}
if (statusCode) {
requests = requests.filter((req) => {
if (statusCode.min && req.status < statusCode.min) return false
if (statusCode.max && req.status > statusCode.max) return false
return true
})
}
const limitedRequests = requests.slice(-limit)
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
total: requests.length,
requests: limitedRequests,
},
null,
2,
),
},
],
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to get network history: ${error.message}`,
},
],
isError: true,
}
}
},
)
// Tool 4: Accessibility Snapshot - Get page accessibility tree as JSON
server.tool('accessibility_snapshot', 'Get the accessibility snapshot of the current page as JSON', {}, async ({}) => {
try {
const { page } = await ensureConnection()
// Check if the method exists
if (typeof (page as any)._snapshotForAI !== 'function') {
// Fall back to regular accessibility snapshot
const snapshot = await page.accessibility.snapshot({
interestingOnly: true,
root: undefined,
})
return {
content: [
{
type: 'text',
text: JSON.stringify(snapshot, null, 2),
},
],
}
}
const snapshot = await (page as any)._snapshotForAI()
return {
content: [
{
type: 'text',
text: snapshot,
},
],
}
} catch (error: any) {
console.error('Accessibility snapshot error:', error)
return {
content: [
{
type: 'text',
text: `Failed to get accessibility snapshot: ${error.message}`,
},
],
isError: true,
}
}
})
// Tool 5: Execute - Run arbitrary JavaScript code with page and context in scope
const promptContent = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'prompt.md'), 'utf-8')
server.tool(
@@ -569,14 +119,15 @@ server.tool(
timeout: z.number().default(3000).describe('Timeout in milliseconds for code execution (default: 3000ms)'),
},
async ({ code, timeout }) => {
const { page } = await ensureConnection()
await ensureConnection()
const page = getCurrentPage()
const context = page.context()
console.error('Executing code:', code)
try {
// Collect console logs during execution
const consoleLogs: Array<{ method: string; args: any[] }> = []
// Create a custom console object that collects logs
const customConsole = {
log: (...args: any[]) => {
consoleLogs.push({ method: 'log', args })
@@ -595,30 +146,27 @@ server.tool(
},
}
// Create a function that has page, context, and console in scope
const executeCode = new Function(
'page',
'context',
'console',
`
return (async () => {
${code}
})();
`,
)
const vmContext = vm.createContext({
page,
context,
state,
console: customConsole,
})
const wrappedCode = `(async () => { ${code} })()`
// Execute the code with page, context, and custom console with timeout
const result = await Promise.race([
executeCode(page, context, customConsole),
vm.runInContext(wrappedCode, vmContext, {
timeout,
displayErrors: true,
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout),
),
])
// Format the response with both console output and return value
let responseText = ''
// Add console logs if any
if (consoleLogs.length > 0) {
responseText += 'Console output:\n'
consoleLogs.forEach(({ method, args }) => {
@@ -635,10 +183,13 @@ server.tool(
responseText += '\n'
}
// Add return value if any
if (result !== undefined) {
responseText += 'Return value:\n'
responseText += JSON.stringify(result, null, 2)
if (typeof result === "string") {
responseText += result
} else {
responseText += JSON.stringify(result, null, 2)
}
} else if (consoleLogs.length === 0) {
responseText += 'Code executed successfully (no output)'
}
@@ -656,7 +207,7 @@ server.tool(
content: [
{
type: 'text',
text: `Error executing code: ${error.message}\n${error.stack}`,
text: `Error executing code: ${error.message}\n${error.stack || ''}`,
},
],
isError: true,
@@ -672,25 +223,17 @@ async function main() {
console.error('Playwright MCP server running on stdio')
}
// Cleanup function
async function cleanup() {
console.error('Shutting down MCP server...')
if (state.browser) {
try {
// Close the browser connection but not the Chrome process
// Since we're using CDP, closing the browser object just closes
// the connection, not the actual Chrome instance
await state.browser.close()
} catch (e) {
// Ignore errors during browser close
}
}
// Don't kill the Chrome process - let it continue running
// The process was started with detached: true and unref()
// so it will persist after this process exits
process.exit(0)
}
@@ -1,202 +1,318 @@
Return value:
- table [ref=e3]:
- rowgroup [ref=e4]:
- row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e5]:
- cell "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e6]:
- row "Hacker Newsnew | threads | past | comments | ask | show | jobs | submit xmorse (70) | logout" [ref=e5]:
- cell "Hacker Newsnew | threads | past | comments | ask | show | jobs | submit xmorse (70) | logout" [ref=e6]:
- table [ref=e7]:
- rowgroup [ref=e8]:
- row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e9]:
- row "Hacker Newsnew | threads | past | comments | ask | show | jobs | submit xmorse (70) | logout" [ref=e9]:
- cell [ref=e10]:
- link [ref=e11] [cursor=pointer]:
- /url: https://news.ycombinator.com
- img [ref=e12] [cursor=pointer]
- cell "Hacker Newsnew | past | comments | ask | show | jobs | submit" [ref=e13]:
- img [ref=e12]
- cell "Hacker Newsnew | threads | past | comments | ask | show | jobs | submit" [ref=e13]:
- generic [ref=e14]:
- link "Hacker News" [ref=e16] [cursor=pointer]:
- /url: news
- link "new" [ref=e17] [cursor=pointer]:
- /url: newest
- text: "|"
- link "past" [ref=e18] [cursor=pointer]:
- link "threads" [ref=e18] [cursor=pointer]:
- /url: threads?id=xmorse
- text: "|"
- link "past" [ref=e19] [cursor=pointer]:
- /url: front
- text: "|"
- link "comments" [ref=e19] [cursor=pointer]:
- link "comments" [ref=e20] [cursor=pointer]:
- /url: newcomments
- text: "|"
- link "ask" [ref=e20] [cursor=pointer]:
- link "ask" [ref=e21] [cursor=pointer]:
- /url: ask
- text: "|"
- link "show" [ref=e21] [cursor=pointer]:
- link "show" [ref=e22] [cursor=pointer]:
- /url: show
- text: "|"
- link "jobs" [ref=e22] [cursor=pointer]:
- link "jobs" [ref=e23] [cursor=pointer]:
- /url: jobs
- text: "|"
- link "submit" [ref=e23] [cursor=pointer]:
- link "submit" [ref=e24] [cursor=pointer]:
- /url: submit
- cell "login" [ref=e24]:
- link "login" [ref=e26] [cursor=pointer]:
- /url: login?goto=item%3Fid%3D1
- row [ref=e27]
- row "upvote Y Combinator (ycombinator.com) 57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments upvote sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR upvote pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road? upvote dmon on Feb 25, 2007 | | [] sure" [ref=e28]:
- cell "upvote Y Combinator (ycombinator.com) 57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments upvote sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR upvote pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road? upvote dmon on Feb 25, 2007 | | [] sure" [ref=e29]:
- table [ref=e30]:
- rowgroup [ref=e31]:
- row "upvote Y Combinator (ycombinator.com)" [ref=e32]:
- cell [ref=e33]
- cell "upvote" [ref=e35]:
- link "upvote" [ref=e37] [cursor=pointer]:
- /url: vote?id=1&how=up&goto=item%3Fid%3D1
- cell "Y Combinator (ycombinator.com)" [ref=e39]:
- generic [ref=e40]:
- link "Y Combinator" [ref=e41] [cursor=pointer]:
- cell "xmorse (70) | logout" [ref=e25]:
- generic [ref=e26]:
- link "xmorse" [ref=e27] [cursor=pointer]:
- /url: user?id=xmorse
- text: (70) |
- link "logout" [ref=e28] [cursor=pointer]:
- /url: logout?auth=227f860372905c327e92615af914e1e41ec43fff&goto=item%3Fid%3D1
- row [ref=e29]
- row [ref=e30]:
- cell [ref=e31]:
- table [ref=e32]:
- rowgroup [ref=e33]:
- row "upvote Y Combinator (ycombinator.com)" [ref=e34]:
- cell [ref=e35]
- cell "upvote" [ref=e36]:
- link "upvote" [ref=e38] [cursor=pointer]:
- /url: vote?id=1&how=up&auth=e52b391ec392eb381ace51126fd80e19d48c1d7b&goto=item%3Fid%3D1
- generic "upvote" [ref=e39]
- cell "Y Combinator (ycombinator.com)" [ref=e40]:
- generic [ref=e41]:
- link "Y Combinator" [ref=e42] [cursor=pointer]:
- /url: http://ycombinator.com
- generic [ref=e42]:
- generic [ref=e43]:
- text: (
- link "ycombinator.com" [ref=e43] [cursor=pointer]:
- link "ycombinator.com" [ref=e44] [cursor=pointer]:
- /url: from?site=ycombinator.com
- generic [ref=e44] [cursor=pointer]: ycombinator.com
- text: )
- row "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e45]:
- row "57 points by pg on Oct 9, 2006 | hide | past | favorite | 16 comments" [ref=e45]:
- cell [ref=e46]
- cell "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e47]:
- cell "57 points by pg on Oct 9, 2006 | hide | past | favorite | 16 comments" [ref=e47]:
- generic [ref=e48]:
- generic [ref=e49]: 57 points
- text: by
- link "pg" [ref=e50] [cursor=pointer]:
- text: 57 points by
- link "pg" [ref=e49] [cursor=pointer]:
- /url: user?id=pg
- link "on Oct 9, 2006" [ref=e52] [cursor=pointer]:
- /url: item?id=1
- generic "2006-10-09T18:21:51 1160418111" [ref=e50]:
- link "on Oct 9, 2006" [ref=e51] [cursor=pointer]:
- /url: item?id=1
- text: "|"
- link "hide" [ref=e54] [cursor=pointer]:
- /url: hide?id=1&goto=item%3Fid%3D1
- link "hide" [ref=e52] [cursor=pointer]:
- /url: hide?id=1&auth=e52b391ec392eb381ace51126fd80e19d48c1d7b&goto=item%3Fid%3D1
- text: "|"
- link "past" [ref=e55] [cursor=pointer]:
- link "past" [ref=e53] [cursor=pointer]:
- /url: https://hn.algolia.com/?query=Y%20Combinator&type=story&dateRange=all&sort=byDate&storyText=false&prefix&page=0
- text: "|"
- link "favorite" [ref=e56] [cursor=pointer]:
- /url: fave?id=1&auth=5328fcfde7333d68e67a8a2334e25acee5599932
- link "favorite" [ref=e54] [cursor=pointer]:
- /url: fave?id=1&auth=e52b391ec392eb381ace51126fd80e19d48c1d7b
- text: "|"
- link "3 comments" [ref=e57] [cursor=pointer]:
- link "16 comments" [ref=e55] [cursor=pointer]:
- /url: item?id=1
- row [ref=e58]:
- cell [ref=e59]
- cell [ref=e60]
- table [ref=e63]:
- rowgroup [ref=e64]:
- row "upvote sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e65]:
- cell "upvote sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e66]:
- table [ref=e67]:
- rowgroup [ref=e68]:
- row "upvote sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e69]:
- cell [ref=e70]:
- row [ref=e56]:
- cell [ref=e57]
- cell [ref=e58]
- table [ref=e59]:
- rowgroup [ref=e60]:
- row "upvote sama on Oct 9, 2006 | [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e61]:
- cell "upvote sama on Oct 9, 2006 | [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e62]:
- table [ref=e63]:
- rowgroup [ref=e64]:
- row "upvote sama on Oct 9, 2006 | [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e65]:
- cell [ref=e66]:
- img
- cell "upvote" [ref=e72]:
- link "upvote" [ref=e74] [cursor=pointer]:
- /url: vote?id=15&how=up&goto=item%3Fid%3D1
- cell "sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e76]:
- generic [ref=e78]:
- link "sama" [ref=e79] [cursor=pointer]:
- cell "upvote" [ref=e67]:
- link "upvote" [ref=e69] [cursor=pointer]:
- /url: vote?id=15&how=up&auth=709295bd69cc8638fcf14a7b807bb0efabd69a2d&goto=item%3Fid%3D1#15
- generic "upvote" [ref=e70]
- cell "sama on Oct 9, 2006 | [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e71]:
- generic [ref=e73]:
- link "sama" [ref=e74] [cursor=pointer]:
- /url: user?id=sama
- link "on Oct 9, 2006" [ref=e81] [cursor=pointer]:
- /url: item?id=15
- link "[]" [ref=e84] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e87]:
- generic [ref=e88]: "\"the rising star of venture capital\" -unknown VC eating lunch on SHR"
- generic "2006-10-09T19:51:01 1160423461" [ref=e75]:
- link "on Oct 9, 2006" [ref=e76] [cursor=pointer]:
- /url: item?id=15
- generic [ref=e77]:
- text: "|"
- link [ref=e78] [cursor=pointer]:
- /url: "#487171"
- text: next
- link "[]" [ref=e79] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e80]:
- generic [ref=e81]: "\"the rising star of venture capital\" -unknown VC eating lunch on SHR"
- generic:
- paragraph
- row "upvote pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road?" [ref=e92]:
- cell "upvote pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road?" [ref=e93]:
- table [ref=e94]:
- rowgroup [ref=e95]:
- row "upvote pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road?" [ref=e96]:
- cell [ref=e97]:
- img [ref=e98]
- cell "upvote" [ref=e99]:
- link "upvote" [ref=e101] [cursor=pointer]:
- /url: vote?id=17&how=up&goto=item%3Fid%3D1
- cell "pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road?" [ref=e103]:
- generic [ref=e105]:
- link "pg" [ref=e106] [cursor=pointer]:
- row "upvote pg on Oct 9, 2006 | | [] Is there anywhere to eat on Sandhill Road?" [ref=e82]:
- cell "upvote pg on Oct 9, 2006 | | [] Is there anywhere to eat on Sandhill Road?" [ref=e83]:
- table [ref=e84]:
- rowgroup [ref=e85]:
- row "upvote pg on Oct 9, 2006 | | [] Is there anywhere to eat on Sandhill Road?" [ref=e86]:
- cell [ref=e87]:
- img [ref=e88]
- cell "upvote" [ref=e89]:
- link "upvote" [ref=e91] [cursor=pointer]:
- /url: vote?id=17&how=up&auth=3c24b126f8f812f8c3a24a0b9143eac42ca15476&goto=item%3Fid%3D1#17
- generic "upvote" [ref=e92]
- cell "pg on Oct 9, 2006 | | [] Is there anywhere to eat on Sandhill Road?" [ref=e93]:
- generic [ref=e95]:
- link "pg" [ref=e96] [cursor=pointer]:
- /url: user?id=pg
- link "on Oct 9, 2006" [ref=e108] [cursor=pointer]:
- /url: item?id=17
- generic [ref=e110]:
- generic "2006-10-09T19:52:45 1160423565" [ref=e97]:
- link "on Oct 9, 2006" [ref=e98] [cursor=pointer]:
- /url: item?id=17
- generic [ref=e99]:
- text: "|"
- link [ref=e111] [cursor=pointer]:
- link [ref=e100] [cursor=pointer]:
- /url: "#15"
- text: parent
- link "[]" [ref=e112] [cursor=pointer]:
- text: "|"
- link [ref=e101] [cursor=pointer]:
- /url: "#487171"
- text: next
- link "[]" [ref=e102] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e115]:
- generic [ref=e116]: Is there anywhere to eat on Sandhill Road?
- generic [ref=e103]:
- generic [ref=e104]: Is there anywhere to eat on Sandhill Road?
- generic:
- paragraph
- row "upvote dmon on Feb 25, 2007 | | [] sure" [ref=e120]:
- cell "upvote dmon on Feb 25, 2007 | | [] sure" [ref=e121]:
- table [ref=e122]:
- rowgroup [ref=e123]:
- row "upvote dmon on Feb 25, 2007 | | [] sure" [ref=e124]:
- cell [ref=e125]:
- img [ref=e126]
- cell "upvote" [ref=e127]:
- link "upvote" [ref=e129] [cursor=pointer]:
- /url: vote?id=1079&how=up&goto=item%3Fid%3D1
- cell "dmon on Feb 25, 2007 | | [] sure" [ref=e131]:
- generic [ref=e133]:
- link "dmon" [ref=e134] [cursor=pointer]:
- row "upvote dmon on Feb 25, 2007 | | | [] sure" [ref=e105]:
- cell "upvote dmon on Feb 25, 2007 | | | [] sure" [ref=e106]:
- table [ref=e107]:
- rowgroup [ref=e108]:
- row "upvote dmon on Feb 25, 2007 | | | [] sure" [ref=e109]:
- cell [ref=e110]:
- img [ref=e111]
- cell "upvote" [ref=e112]:
- link "upvote" [ref=e114] [cursor=pointer]:
- /url: vote?id=1079&how=up&auth=851f0b1e178c46300e9d2218764827726415a50d&goto=item%3Fid%3D1#1079
- generic "upvote" [ref=e115]
- cell "dmon on Feb 25, 2007 | | | [] sure" [ref=e116]:
- generic [ref=e118]:
- link "dmon" [ref=e119] [cursor=pointer]:
- /url: user?id=dmon
- link "on Feb 25, 2007" [ref=e136] [cursor=pointer]:
- /url: item?id=1079
- generic [ref=e138]:
- generic "2007-02-25T22:18:23 1172441903" [ref=e120]:
- link "on Feb 25, 2007" [ref=e121] [cursor=pointer]:
- /url: item?id=1079
- generic [ref=e122]:
- text: "|"
- link [ref=e139] [cursor=pointer]:
- link [ref=e123] [cursor=pointer]:
- /url: "#15"
- text: root
- text: "|"
- link [ref=e140] [cursor=pointer]:
- link [ref=e124] [cursor=pointer]:
- /url: "#17"
- text: parent
- link "[]" [ref=e141] [cursor=pointer]:
- text: "|"
- link [ref=e125] [cursor=pointer]:
- /url: "#487171"
- text: next
- link "[]" [ref=e126] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e144]:
- generic [ref=e145]: sure
- generic [ref=e127]:
- generic [ref=e128]: sure
- generic:
- paragraph
- row "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4 Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e151]:
- cell "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4 Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e152]:
- row "jacquesm on Feb 19, 2009 [dead] | | [] So, just to see how hard it is to make the longest span between article and comment :) Congratulations on your second birthday YC, and thanks to Paul Graham for writing this forum. I had a really good look at the good a few days ago and I was quite impressed with how elegant the whole thing is put together. Lisp would not be my language of choice for a website like this, and yet, after seeing how concise it was I'm tempted to play around with lisp in a web environment." [ref=e129]:
- cell "jacquesm on Feb 19, 2009 [dead] | | [] So, just to see how hard it is to make the longest span between article and comment :) Congratulations on your second birthday YC, and thanks to Paul Graham for writing this forum. I had a really good look at the good a few days ago and I was quite impressed with how elegant the whole thing is put together. Lisp would not be my language of choice for a website like this, and yet, after seeing how concise it was I'm tempted to play around with lisp in a web environment." [ref=e130]:
- table [ref=e131]:
- rowgroup [ref=e132]:
- row "jacquesm on Feb 19, 2009 [dead] | | [] So, just to see how hard it is to make the longest span between article and comment :) Congratulations on your second birthday YC, and thanks to Paul Graham for writing this forum. I had a really good look at the good a few days ago and I was quite impressed with how elegant the whole thing is put together. Lisp would not be my language of choice for a website like this, and yet, after seeing how concise it was I'm tempted to play around with lisp in a web environment." [ref=e133]:
- cell [ref=e134]:
- img
- cell [ref=e135]:
- img [ref=e137]
- cell "jacquesm on Feb 19, 2009 [dead] | | [] So, just to see how hard it is to make the longest span between article and comment :) Congratulations on your second birthday YC, and thanks to Paul Graham for writing this forum. I had a really good look at the good a few days ago and I was quite impressed with how elegant the whole thing is put together. Lisp would not be my language of choice for a website like this, and yet, after seeing how concise it was I'm tempted to play around with lisp in a web environment." [ref=e138]:
- generic [ref=e140]:
- link "jacquesm" [ref=e141] [cursor=pointer]:
- /url: user?id=jacquesm
- generic "2009-02-19T12:21:23 1235046083" [ref=e142]:
- link "on Feb 19, 2009" [ref=e143] [cursor=pointer]:
- /url: item?id=487171
- text: "[dead]"
- generic [ref=e144]:
- text: "|"
- link [ref=e145] [cursor=pointer]:
- /url: "#15"
- text: prev
- text: "|"
- link [ref=e146] [cursor=pointer]:
- /url: "#234509"
- text: next
- link "[]" [ref=e147] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e148]:
- generic [ref=e149]:
- text: So, just to see how hard it is to make the longest span between article and comment :)
- paragraph [ref=e150]: Congratulations on your second birthday YC, and thanks to Paul Graham for writing this forum. I had a really good look at the good a few days ago and I was quite impressed with how elegant the whole thing is put together.
- paragraph [ref=e151]: Lisp would not be my language of choice for a website like this, and yet, after seeing how concise it was I'm tempted to play around with lisp in a web environment.
- generic:
- paragraph
- row "kleevr on July 2, 2008 [dead] | | [11 more]" [ref=e152]:
- cell "kleevr on July 2, 2008 [dead] | | [11 more]" [ref=e153]:
- table [ref=e154]:
- rowgroup [ref=e155]:
- row "kleevr on July 2, 2008 [dead] | | [11 more]" [ref=e156]:
- cell [ref=e157]:
- img
- cell "kleevr on July 2, 2008 [dead] | | [11 more]" [ref=e158]:
- generic [ref=e160]:
- link "kleevr" [ref=e161] [cursor=pointer]:
- /url: user?id=kleevr
- generic "2008-07-02T20:29:48 1215030588" [ref=e162]:
- link "on July 2, 2008" [ref=e163] [cursor=pointer]:
- /url: item?id=234509
- text: "[dead]"
- generic [ref=e164]:
- text: "|"
- link [ref=e165] [cursor=pointer]:
- /url: "#487171"
- text: prev
- text: "|"
- link [ref=e166] [cursor=pointer]:
- /url: "#82729"
- text: next
- link "[11 more]" [ref=e167] [cursor=pointer]:
- /url: javascript:void(0)
- row "vice on Nov 22, 2007 [dead] | [] I'm nX 1 too ;)" [ref=e168]:
- cell "vice on Nov 22, 2007 [dead] | [] I'm nX 1 too ;)" [ref=e169]:
- table [ref=e170]:
- rowgroup [ref=e171]:
- row "vice on Nov 22, 2007 [dead] | [] I'm nX 1 too ;)" [ref=e172]:
- cell [ref=e173]:
- img
- cell [ref=e174]:
- img [ref=e176]
- cell "vice on Nov 22, 2007 [dead] | [] I'm nX 1 too ;)" [ref=e177]:
- generic [ref=e179]:
- link "vice" [ref=e180] [cursor=pointer]:
- /url: user?id=vice
- generic "2007-11-22T12:50:54 1195735854" [ref=e181]:
- link "on Nov 22, 2007" [ref=e182] [cursor=pointer]:
- /url: item?id=82729
- text: "[dead]"
- generic [ref=e183]:
- text: "|"
- link [ref=e184] [cursor=pointer]:
- /url: "#234509"
- text: prev
- link "[]" [ref=e185] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e186]:
- generic [ref=e187]:
- text: I'm nX 1 too
- paragraph [ref=e188]: ;)
- generic:
- paragraph
- row "Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e189]:
- cell "Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e190]:
- img
- table [ref=e154]:
- rowgroup [ref=e155]:
- row [ref=e156]:
- cell [ref=e157]
- link "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4" [ref=e160] [cursor=pointer]:
- /url: https://www.ycombinator.com/apply/
- generic [ref=e162]:
- generic [ref=e163]:
- link "Guidelines" [ref=e164] [cursor=pointer]:
- table [ref=e191]:
- rowgroup [ref=e192]:
- row [ref=e193]:
- cell [ref=e194]
- generic [ref=e195]:
- generic [ref=e196]:
- link "Guidelines" [ref=e197] [cursor=pointer]:
- /url: newsguidelines.html
- text: "|"
- link "FAQ" [ref=e165] [cursor=pointer]:
- link "FAQ" [ref=e198] [cursor=pointer]:
- /url: newsfaq.html
- text: "|"
- link "Lists" [ref=e166] [cursor=pointer]:
- link "Lists" [ref=e199] [cursor=pointer]:
- /url: lists
- text: "|"
- link "API" [ref=e167] [cursor=pointer]:
- link "API" [ref=e200] [cursor=pointer]:
- /url: https://github.com/HackerNews/API
- text: "|"
- link "Security" [ref=e168] [cursor=pointer]:
- link "Security" [ref=e201] [cursor=pointer]:
- /url: security.html
- text: "|"
- link "Legal" [ref=e169] [cursor=pointer]:
- link "Legal" [ref=e202] [cursor=pointer]:
- /url: https://www.ycombinator.com/legal/
- text: "|"
- link "Apply to YC" [ref=e170] [cursor=pointer]:
- link "Apply to YC" [ref=e203] [cursor=pointer]:
- /url: https://www.ycombinator.com/apply/
- text: "|"
- link "Contact" [ref=e171] [cursor=pointer]:
- link "Contact" [ref=e204] [cursor=pointer]:
- /url: mailto:hn@ycombinator.com
- generic [ref=e174]:
- generic [ref=e205]:
- text: "Search:"
- textbox [ref=e175]
- textbox [ref=e206]
@@ -1,3 +1,4 @@
Return value:
- generic [active] [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
@@ -5,549 +6,338 @@
- link "shadcn/ui" [ref=e6] [cursor=pointer]:
- /url: /
- img
- generic [ref=e11] [cursor=pointer]: shadcn/ui
- navigation [ref=e12]:
- link "Docs" [ref=e13] [cursor=pointer]:
- generic [ref=e7]: shadcn/ui
- navigation [ref=e8]:
- link "Docs" [ref=e9] [cursor=pointer]:
- /url: /docs/installation
- link "Components" [ref=e14] [cursor=pointer]:
- link "Components" [ref=e10] [cursor=pointer]:
- /url: /docs/components
- link "Blocks" [ref=e15] [cursor=pointer]:
- link "Blocks" [ref=e11] [cursor=pointer]:
- /url: /blocks
- link "Charts" [ref=e16] [cursor=pointer]:
- link "Charts" [ref=e12] [cursor=pointer]:
- /url: /charts/area
- link "Themes" [ref=e17] [cursor=pointer]:
- link "Directory" [ref=e13] [cursor=pointer]:
- /url: /docs/directory
- link "Themes" [ref=e14] [cursor=pointer]:
- /url: /themes
- link "Colors" [ref=e18] [cursor=pointer]:
- link "Colors" [ref=e15] [cursor=pointer]:
- /url: /colors
- generic [ref=e19]:
- button "Search documentation... ⌘ K" [ref=e21]:
- generic [ref=e22]: Search documentation...
- generic [ref=e23]:
- generic [ref=e16]:
- button "Search documentation... ⌘ K" [ref=e18]:
- generic [ref=e19]: Search documentation...
- generic [ref=e21]:
- generic: ⌘
- generic: K
- link "91.7k" [ref=e26] [cursor=pointer]:
- link "98.7k" [ref=e22] [cursor=pointer]:
- /url: https://github.com/shadcn-ui/ui
- img
- generic [ref=e29] [cursor=pointer]: 91.7k
- button "Toggle layout" [ref=e30]:
- generic [ref=e31]: Toggle layout
- generic [ref=e23]: 98.7k
- button "Toggle layout" [ref=e24]:
- generic [ref=e25]: Toggle layout
- img
- button "Toggle theme" [ref=e36]:
- button "Toggle theme" [ref=e26]:
- img
- generic [ref=e44]: Toggle theme
- main [ref=e45]:
- generic [ref=e46]:
- generic [ref=e49]:
- link "New Calendar Component" [ref=e50] [cursor=pointer]:
- /url: /docs/components/calendar
- text: New Calendar Component
- generic [ref=e27]: Toggle theme
- main [ref=e28]:
- generic [ref=e29]:
- generic [ref=e32]:
- 'link "New New Components: Field, Input Group, Item and more" [ref=e33] [cursor=pointer]':
- /url: /docs/changelog
- generic "New" [ref=e34]
- text: "New Components: Field, Input Group, Item and more"
- img
- heading "The Foundation for your Design System" [level=1] [ref=e54]
- paragraph [ref=e55]: A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code.
- generic [ref=e56]:
- link "Get Started" [ref=e57] [cursor=pointer]:
- heading "The Foundation for your Design System" [level=1] [ref=e35]
- paragraph [ref=e36]: A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code.
- generic [ref=e37]:
- link "Get Started" [ref=e38] [cursor=pointer]:
- /url: /docs/installation
- link "View Components" [ref=e58] [cursor=pointer]:
- link "View Components" [ref=e39] [cursor=pointer]:
- /url: /docs/components
- generic [ref=e60]:
- generic [ref=e65]:
- link "Examples" [ref=e66] [cursor=pointer]:
- generic [ref=e41]:
- generic [ref=e46]:
- link "Examples" [ref=e47] [cursor=pointer]:
- /url: /
- link "Dashboard" [ref=e67] [cursor=pointer]:
- link "Dashboard" [ref=e48] [cursor=pointer]:
- /url: /examples/dashboard
- link "Tasks" [ref=e68] [cursor=pointer]:
- link "Tasks" [ref=e49] [cursor=pointer]:
- /url: /examples/tasks
- link "Playground" [ref=e69] [cursor=pointer]:
- link "Playground" [ref=e50] [cursor=pointer]:
- /url: /examples/playground
- link "Authentication" [ref=e70] [cursor=pointer]:
- link "Authentication" [ref=e51] [cursor=pointer]:
- /url: /examples/authentication
- generic [ref=e71]:
- generic [ref=e72]: Theme
- combobox "Theme" [ref=e73]:
- generic [ref=e74]: "Theme:"
- generic: Default
- generic [ref=e52]:
- generic [ref=e53]: Theme
- combobox "Theme" [ref=e54]:
- generic [ref=e55]: "Theme:"
- generic: Neutral
- img
- generic [ref=e81]:
- generic [ref=e82]:
- generic [ref=e83]:
- generic [ref=e84]:
- generic [ref=e85]:
- generic [ref=e86]: Total Revenue
- generic [ref=e87]: $15,231.89
- generic [ref=e88]: +20.1% from last month
- img [ref=e93]
- generic [ref=e99]:
- generic [ref=e100]:
- generic [ref=e101]: Subscriptions
- generic [ref=e102]: +2,350
- generic [ref=e103]: +180.1% from last month
- button "View More" [ref=e105]
- img [ref=e110]
- generic [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- generic [ref=e127]: Upgrade your subscription
- generic [ref=e128]: You are currently on the free plan. Upgrade to the pro plan to get access to all features.
- generic [ref=e130]:
- generic [ref=e131]:
- generic [ref=e132]:
- generic [ref=e133]: Name
- textbox "Name" [ref=e134]
- generic [ref=e135]:
- generic [ref=e136]: Email
- textbox "Email" [ref=e137]
- generic [ref=e138]:
- generic [ref=e139]: Card Number
- generic [ref=e140]:
- textbox "Card Number" [ref=e141]
- textbox "MM/YY" [ref=e142]
- textbox "CVC" [ref=e143]
- group "Plan" [ref=e144]:
- generic [ref=e145]: Plan
- paragraph [ref=e146]: Select the plan that best fits your needs.
- radiogroup [ref=e147]:
- generic [ref=e148]:
- radio "Starter Plan Perfect for small businesses." [checked] [ref=e149]:
- img [ref=e151]
- generic [ref=e153]:
- generic [ref=e154]: Starter Plan
- generic [ref=e155]: Perfect for small businesses.
- generic [ref=e156]:
- radio "Pro Plan More features and storage." [ref=e157]
- generic [ref=e158]:
- generic [ref=e159]: Pro Plan
- generic [ref=e160]: More features and storage.
- generic [ref=e161]:
- generic [ref=e162]: Notes
- textbox "Notes" [ref=e163]
- generic [ref=e164]:
- generic [ref=e165]:
- checkbox "I agree to the terms and conditions" [ref=e166]
- generic [ref=e167]: I agree to the terms and conditions
- generic [ref=e168]:
- checkbox "Allow us to send you emails" [checked] [ref=e169]:
- generic:
- img
- generic [ref=e173]: Allow us to send you emails
- generic [ref=e174]:
- button "Cancel" [ref=e175]
- button "Upgrade Plan" [ref=e176]
- generic [ref=e177]:
- generic [ref=e178]:
- generic [ref=e179]: Team Members
- generic [ref=e180]: Invite your team members to collaborate.
- generic [ref=e181]:
- generic [ref=e182]:
- generic [ref=e183]:
- img "Image" [ref=e185]
- generic [ref=e186]:
- paragraph [ref=e187]: Sofia Davis
- paragraph [ref=e188]: m@example.com
- button "Owner" [ref=e189]:
- text: Owner
- img
- generic [ref=e192]:
- generic [ref=e193]:
- img "Image" [ref=e195]
- generic [ref=e196]:
- paragraph [ref=e197]: Jackson Lee
- paragraph [ref=e198]: p@example.com
- button "Developer" [ref=e199]:
- text: Developer
- img
- generic [ref=e202]:
- generic [ref=e203]:
- img "Image" [ref=e205]
- generic [ref=e206]:
- paragraph [ref=e207]: Isabella Nguyen
- paragraph [ref=e208]: i@example.com
- button "Billing" [ref=e209]:
- text: Billing
- img
- generic [ref=e212]:
- generic [ref=e213]:
- generic [ref=e214]: Cookie Settings
- generic [ref=e215]: Manage your cookie settings here.
- generic [ref=e216]:
- generic [ref=e217]:
- generic [ref=e218]:
- generic [ref=e219]: Strictly Necessary
- generic [ref=e220]: These cookies are essential in order to use the website and use its features.
- switch "Necessary" [checked] [ref=e221]
- generic [ref=e223]:
- generic [ref=e224]:
- generic [ref=e225]: Functional Cookies
- generic [ref=e226]: These cookies allow the website to provide personalized functionality.
- switch "Functional" [ref=e227]
- button "Save preferences" [ref=e230]
- generic [ref=e231]:
- generic [ref=e232]:
- generic [ref=e233]:
- generic [ref=e234]: Create an account
- generic [ref=e235]: Enter your email below to create your account
- generic [ref=e236]:
- generic [ref=e237]:
- button "GitHub" [ref=e238]:
- img
- text: GitHub
- button "Google" [ref=e241]:
- img
- text: Google
- generic [ref=e248]: Or continue with
- generic [ref=e249]:
- generic [ref=e250]: Email
- textbox "Email" [ref=e251]
- generic [ref=e252]:
- generic [ref=e253]: Password
- textbox "Password" [ref=e254]
- button "Create account" [ref=e256]
- generic [ref=e257]:
- generic [ref=e258]:
- generic [ref=e259]:
- img "Image" [ref=e261]
- generic [ref=e262]:
- paragraph [ref=e263]: Sofia Davis
- paragraph [ref=e264]: m@example.com
- button "New message" [ref=e265]:
- button "Copy Code" [ref=e56]:
- img
- generic [ref=e57]: Copy Code
- generic [ref=e61]:
- generic [ref=e65]:
- group "Payment Method" [ref=e66]:
- generic [ref=e67]: Payment Method
- paragraph [ref=e68]: All transactions are secure and encrypted
- generic [ref=e69]:
- group [ref=e70]:
- generic [ref=e71]: Name on Card
- textbox "Name on Card" [ref=e72]:
- /placeholder: John Doe
- generic [ref=e73]:
- group [ref=e74]:
- generic [ref=e75]: Card Number
- textbox "Card Number" [ref=e76]:
- /placeholder: 1234 5678 9012 3456
- paragraph [ref=e77]: Enter your 16-digit number.
- group [ref=e78]:
- generic [ref=e79]: CVV
- textbox "CVV" [ref=e80]:
- /placeholder: "123"
- generic [ref=e81]:
- group [ref=e82]:
- generic [ref=e83]: Month
- combobox "Month" [ref=e84]:
- generic: MM
- img
- generic [ref=e269]: New message
- generic [ref=e271]:
- generic [ref=e272]: Hi, how can I help you today?
- generic [ref=e273]: Hey, I'm having trouble with my account.
- generic [ref=e274]: What seems to be the problem?
- generic [ref=e275]: I can't log in.
- generic [ref=e277]:
- textbox "Type your message..." [ref=e278]
- button "Send" [disabled]:
- combobox [ref=e85]
- group [ref=e86]:
- generic [ref=e87]: Year
- combobox "Year" [ref=e88]:
- generic: YYYY
- img
- generic: Send
- generic [ref=e285]:
- generic [ref=e286]:
- generic [ref=e287]: Report an issue
- generic [ref=e288]: What area are you having problems with?
- generic [ref=e289]:
- generic [ref=e290]:
- generic [ref=e291]:
- generic [ref=e292]: Area
- combobox "Area" [ref=e293]:
- generic: Billing
- img
- generic [ref=e297]:
- generic [ref=e298]: Security Level
- combobox "Security Level" [ref=e299]:
- generic: Severity 2
- img
- generic [ref=e303]:
- generic [ref=e304]: Subject
- textbox "Subject" [ref=e305]
- generic [ref=e306]:
- generic [ref=e307]: Description
- textbox "Description" [ref=e308]
- generic [ref=e309]:
- button "Cancel" [ref=e310]
- button "Submit" [ref=e311]
- generic [ref=e312]:
- generic [ref=e313]:
- generic [ref=e317]:
- navigation [ref=e318]:
- button "Go to the Previous Month" [ref=e319]:
- img
- button "Go to the Next Month" [ref=e322]:
- img
- generic [ref=e325]:
- status [ref=e327]: June 2025
- grid "June 2025" [ref=e328]:
- rowgroup [ref=e329]:
- row [ref=e330]:
- columnheader [ref=e331]: Su
- columnheader [ref=e332]: Mo
- columnheader [ref=e333]: Tu
- columnheader [ref=e334]: We
- columnheader [ref=e335]: Th
- columnheader [ref=e336]: Fr
- columnheader [ref=e337]: Sa
- rowgroup [ref=e338]:
- row "Sunday, June 1st, 2025 Monday, June 2nd, 2025 Tuesday, June 3rd, 2025 Wednesday, June 4th, 2025 Thursday, June 5th, 2025, selected Friday, June 6th, 2025, selected Saturday, June 7th, 2025, selected" [ref=e339]:
- gridcell "Sunday, June 1st, 2025" [ref=e340]:
- button "Sunday, June 1st, 2025" [ref=e341]: "1"
- gridcell "Monday, June 2nd, 2025" [ref=e342]:
- button "Monday, June 2nd, 2025" [ref=e343]: "2"
- gridcell "Tuesday, June 3rd, 2025" [ref=e344]:
- button "Tuesday, June 3rd, 2025" [ref=e345]: "3"
- gridcell "Wednesday, June 4th, 2025" [ref=e346]:
- button "Wednesday, June 4th, 2025" [ref=e347]: "4"
- gridcell "Thursday, June 5th, 2025, selected" [selected] [ref=e348]:
- button "Thursday, June 5th, 2025, selected" [ref=e349]: "5"
- gridcell "Friday, June 6th, 2025, selected" [selected] [ref=e350]:
- button "Friday, June 6th, 2025, selected" [ref=e351]: "6"
- gridcell "Saturday, June 7th, 2025, selected" [selected] [ref=e352]:
- button "Saturday, June 7th, 2025, selected" [ref=e353]: "7"
- row "Sunday, June 8th, 2025, selected Monday, June 9th, 2025, selected Tuesday, June 10th, 2025, selected Wednesday, June 11th, 2025, selected Thursday, June 12th, 2025, selected Friday, June 13th, 2025, selected Saturday, June 14th, 2025" [ref=e354]:
- gridcell "Sunday, June 8th, 2025, selected" [selected] [ref=e355]:
- button "Sunday, June 8th, 2025, selected" [ref=e356]: "8"
- gridcell "Monday, June 9th, 2025, selected" [selected] [ref=e357]:
- button "Monday, June 9th, 2025, selected" [ref=e358]: "9"
- gridcell "Tuesday, June 10th, 2025, selected" [selected] [ref=e359]:
- button "Tuesday, June 10th, 2025, selected" [ref=e360]: "10"
- gridcell "Wednesday, June 11th, 2025, selected" [selected] [ref=e361]:
- button "Wednesday, June 11th, 2025, selected" [ref=e362]: "11"
- gridcell "Thursday, June 12th, 2025, selected" [selected] [ref=e363]:
- button "Thursday, June 12th, 2025, selected" [ref=e364]: "12"
- gridcell "Friday, June 13th, 2025, selected" [selected] [ref=e365]:
- button "Friday, June 13th, 2025, selected" [ref=e366]: "13"
- gridcell "Saturday, June 14th, 2025" [ref=e367]:
- button "Saturday, June 14th, 2025" [ref=e368]: "14"
- row "Sunday, June 15th, 2025 Monday, June 16th, 2025 Tuesday, June 17th, 2025 Wednesday, June 18th, 2025 Thursday, June 19th, 2025 Friday, June 20th, 2025 Saturday, June 21st, 2025" [ref=e369]:
- gridcell "Sunday, June 15th, 2025" [ref=e370]:
- button "Sunday, June 15th, 2025" [ref=e371]: "15"
- gridcell "Monday, June 16th, 2025" [ref=e372]:
- button "Monday, June 16th, 2025" [ref=e373]: "16"
- gridcell "Tuesday, June 17th, 2025" [ref=e374]:
- button "Tuesday, June 17th, 2025" [ref=e375]: "17"
- gridcell "Wednesday, June 18th, 2025" [ref=e376]:
- button "Wednesday, June 18th, 2025" [ref=e377]: "18"
- gridcell "Thursday, June 19th, 2025" [ref=e378]:
- button "Thursday, June 19th, 2025" [ref=e379]: "19"
- gridcell "Friday, June 20th, 2025" [ref=e380]:
- button "Friday, June 20th, 2025" [ref=e381]: "20"
- gridcell "Saturday, June 21st, 2025" [ref=e382]:
- button "Saturday, June 21st, 2025" [ref=e383]: "21"
- row "Sunday, June 22nd, 2025 Monday, June 23rd, 2025 Tuesday, June 24th, 2025 Wednesday, June 25th, 2025 Thursday, June 26th, 2025 Friday, June 27th, 2025 Saturday, June 28th, 2025" [ref=e384]:
- gridcell "Sunday, June 22nd, 2025" [ref=e385]:
- button "Sunday, June 22nd, 2025" [ref=e386]: "22"
- gridcell "Monday, June 23rd, 2025" [ref=e387]:
- button "Monday, June 23rd, 2025" [ref=e388]: "23"
- gridcell "Tuesday, June 24th, 2025" [ref=e389]:
- button "Tuesday, June 24th, 2025" [ref=e390]: "24"
- gridcell "Wednesday, June 25th, 2025" [ref=e391]:
- button "Wednesday, June 25th, 2025" [ref=e392]: "25"
- gridcell "Thursday, June 26th, 2025" [ref=e393]:
- button "Thursday, June 26th, 2025" [ref=e394]: "26"
- gridcell "Friday, June 27th, 2025" [ref=e395]:
- button "Friday, June 27th, 2025" [ref=e396]: "27"
- gridcell "Saturday, June 28th, 2025" [ref=e397]:
- button "Saturday, June 28th, 2025" [ref=e398]: "28"
- row "Sunday, June 29th, 2025 Monday, June 30th, 2025 Tuesday, July 1st, 2025 Wednesday, July 2nd, 2025 Thursday, July 3rd, 2025 Friday, July 4th, 2025 Saturday, July 5th, 2025" [ref=e399]:
- gridcell "Sunday, June 29th, 2025" [ref=e400]:
- button "Sunday, June 29th, 2025" [ref=e401]: "29"
- gridcell "Monday, June 30th, 2025" [ref=e402]:
- button "Monday, June 30th, 2025" [ref=e403]: "30"
- gridcell "Tuesday, July 1st, 2025" [ref=e404]:
- button "Tuesday, July 1st, 2025" [ref=e405]: "1"
- gridcell "Wednesday, July 2nd, 2025" [ref=e406]:
- button "Wednesday, July 2nd, 2025" [ref=e407]: "2"
- gridcell "Thursday, July 3rd, 2025" [ref=e408]:
- button "Thursday, July 3rd, 2025" [ref=e409]: "3"
- gridcell "Friday, July 4th, 2025" [ref=e410]:
- button "Friday, July 4th, 2025" [ref=e411]: "4"
- gridcell "Saturday, July 5th, 2025" [ref=e412]:
- button "Saturday, July 5th, 2025" [ref=e413]: "5"
- generic [ref=e415]:
- generic [ref=e416]:
- generic [ref=e417]: Move Goal
- generic [ref=e418]: Set your daily activity goal.
- generic [ref=e419]:
- generic [ref=e420]:
- button "Decrease" [ref=e421]:
- img
- generic [ref=e424]: Decrease
- generic [ref=e425]:
- generic [ref=e426]: "350"
- generic [ref=e427]: Calories/day
- button "Increase" [ref=e428]:
- img
- generic [ref=e432]: Increase
- img [ref=e437]
- button "Set Goal" [ref=e472]
- generic [ref=e474]:
- generic [ref=e475]:
- generic [ref=e476]: Exercise Minutes
- generic [ref=e477]: Your exercise minutes are ahead of where you normally are.
- generic [ref=e481]:
- application [ref=e482]:
- generic [ref=e494]:
- generic [ref=e497]: Mon
- generic [ref=e500]: Tue
- generic [ref=e503]: Wed
- generic [ref=e506]: Thu
- generic [ref=e509]: Fri
- generic [ref=e512]: Sat
- generic [ref=e515]: Sun
- combobox [ref=e89]
- group "Billing Address" [ref=e91]:
- generic [ref=e92]: Billing Address
- paragraph [ref=e93]: The billing address associated with your payment method
- group [ref=e95]:
- checkbox "Same as shipping address" [checked] [ref=e96]:
- generic:
- img
- checkbox [checked]
- generic [ref=e97]: Same as shipping address
- group [ref=e99]:
- group [ref=e101]:
- generic [ref=e102]: Comments
- textbox "Comments" [ref=e103]:
- /placeholder: Add any additional comments
- group [ref=e104]:
- button "Submit" [ref=e105]
- button "Cancel" [ref=e106]
- generic [ref=e107]:
- generic [ref=e108]:
- generic [ref=e109]:
- generic [ref=e111]:
- img "@shadcn" [ref=e113]
- img "@maxleiter" [ref=e115]
- img "@evilrabbit" [ref=e117]
- generic [ref=e118]: No Team Members
- generic [ref=e119]: Invite your team to collaborate on this project.
- button "Invite Members" [ref=e121]:
- img
- text: Invite Members
- generic [ref=e122]:
- generic [ref=e123]:
- status "Loading"
- text: Syncing
- generic [ref=e124]:
- status "Loading"
- text: Updating
- generic [ref=e125]:
- status "Loading"
- text: Loading
- group [ref=e126]:
- group [ref=e127]:
- button "Add" [ref=e128]:
- img
- group [ref=e129]:
- group [ref=e130]:
- textbox "Send a message..." [ref=e131]
- group [ref=e132]:
- button "Voice Mode" [ref=e133]:
- img
- group [ref=e135]:
- generic [ref=e136]: Price Range
- paragraph [ref=e137]: Set your budget range ($200 - 800).
- generic "Price Range" [ref=e138]:
- slider "Minimum" [ref=e142]
- slider "Maximum" [ref=e144]
- generic [ref=e145]:
- group [ref=e146]:
- textbox "Search..." [ref=e147]
- group [ref=e148]:
- img [ref=e149]
- group [ref=e152]: 12 results
- group [ref=e153]:
- textbox "example.com" [ref=e154]
- group [ref=e155]:
- generic [ref=e156]: https://
- group [ref=e157]:
- button "Info" [ref=e158]:
- img
- group [ref=e159]:
- textbox "Ask, Search or Chat..." [ref=e160]
- group [ref=e161]:
- button "Add" [ref=e162]:
- img
- button "Auto" [ref=e163]
- generic [ref=e164]: 52% used
- button "Send" [ref=e165]:
- img
- generic [ref=e166]: Send
- group [ref=e167]:
- textbox "@shadcn" [ref=e168]
- group [ref=e169]:
- img [ref=e171]
- generic [ref=e173]:
- generic [ref=e174]:
- generic [ref=e175]: Input Secure
- group [ref=e176]:
- textbox "Input Secure" [ref=e177]
- group [ref=e178]:
- button "Info" [ref=e179]:
- img
- group [ref=e180]: https://
- group [ref=e181]:
- button "Favorite" [ref=e182]:
- img
- generic [ref=e183]:
- generic [ref=e184]:
- generic [ref=e185]:
- generic [ref=e186]: Two-factor authentication
- paragraph [ref=e187]: Verify via email or phone number.
- button "Enable" [ref=e189]
- link "Your profile has been verified." [ref=e190] [cursor=pointer]:
- /url: "#"
- generic [ref=e191]:
- img
- generic [ref=e193]: Your profile has been verified.
- img [ref=e195]
- generic [ref=e198]: Appearance Settings
- group [ref=e199]:
- generic [ref=e200]:
- group "Compute Environment" [ref=e201]:
- generic [ref=e202]: Compute Environment
- paragraph [ref=e203]: Select the compute environment for your cluster.
- radiogroup [ref=e204]:
- group [ref=e206]:
- generic [ref=e207]:
- generic [ref=e208]: Kubernetes
- paragraph [ref=e209]: Run GPU workloads on a K8s configured cluster. This is the default.
- radio "Kubernetes" [checked] [ref=e210]:
- img [ref=e211]
- group [ref=e214]:
- generic [ref=e215]:
- generic [ref=e216]: Virtual Machine
- paragraph [ref=e217]: Access a VM configured cluster to run workloads. (Coming soon)
- radio "Virtual Machine" [ref=e218]
- group [ref=e220]:
- generic [ref=e221]:
- generic [ref=e222]: Number of GPUs
- paragraph [ref=e223]: You can add more later.
- group [ref=e224]:
- textbox "Number of GPUs" [ref=e225]: "8"
- button "Decrement" [ref=e226]:
- img
- button "Increment" [ref=e227]:
- img
- group [ref=e229]:
- generic [ref=e230]:
- generic [ref=e231]: Wallpaper Tinting
- paragraph [ref=e232]: Allow the wallpaper to be tinted.
- switch "Wallpaper Tinting" [checked] [ref=e233]
- generic [ref=e234]:
- group [ref=e236]:
- generic [ref=e237]: Prompt
- group [ref=e238]:
- textbox "Prompt" [ref=e239]:
- /placeholder: Ask, search, or make anything...
- group [ref=e240]:
- button "Add context" [ref=e241]:
- img
- text: Add context
- group [ref=e243]:
- button "Attach file" [ref=e244]:
- img
- button "Auto" [ref=e245]
- button "All Sources" [ref=e246]:
- img
- text: All Sources
- button "Send" [ref=e247]:
- img
- group [ref=e248]:
- group [ref=e249]:
- button "Go Back" [ref=e250]:
- img
- group [ref=e251]:
- button "Archive" [ref=e252]
- button "Report" [ref=e253]
- group [ref=e254]:
- button "Snooze" [ref=e255]
- button "More Options" [ref=e256]:
- img
- group [ref=e258]:
- checkbox "I agree to the terms and conditions" [checked] [ref=e259]:
- generic:
- img
- generic [ref=e260]: I agree to the terms and conditions
- generic [ref=e261]:
- group [ref=e262]:
- group [ref=e263]:
- button "1" [ref=e264]
- button "2" [ref=e265]
- button "3" [ref=e266]
- group [ref=e267]:
- button "Previous" [ref=e268]:
- img
- button "Next" [ref=e269]:
- img
- group [ref=e270]:
- button "Copilot" [ref=e271]:
- img
- text: Copilot
- button "Open Popover" [ref=e272]:
- img
- group "How did you hear about us?" [ref=e277]:
- generic [ref=e278]: How did you hear about us?
- paragraph [ref=e279]: Select the option that best describes how you heard about us.
- generic [ref=e280]:
- group [ref=e282]:
- checkbox "Social Media" [checked] [ref=e283]:
- generic:
- generic: Thursday
- generic:
- generic:
- generic:
- generic:
- generic: Today
- generic: "390"
- generic:
- generic:
- generic:
- generic: Average
- generic: "278"
- generic [ref=e542]:
- generic [ref=e543]:
- generic [ref=e544]: Payments
- generic [ref=e545]: Manage your payments.
- button "Add Payment" [ref=e547]
- generic [ref=e548]:
- table [ref=e551]:
- rowgroup [ref=e552]:
- row "Select all Status Email Amount" [ref=e553]:
- cell "Select all" [ref=e554]:
- checkbox "Select all" [ref=e555]
- cell "Status" [ref=e556]
- cell "Email" [ref=e557]
- cell "Amount" [ref=e558]:
- generic [ref=e559]: Amount
- cell [ref=e560]
- rowgroup [ref=e561]:
- row "Select row success ken99@example.com $316.00 Open menu" [ref=e562]:
- cell "Select row" [ref=e563]:
- checkbox "Select row" [ref=e564]
- cell "success" [ref=e565]:
- generic [ref=e566]: success
- cell "ken99@example.com" [ref=e567]:
- generic [ref=e568]: ken99@example.com
- cell "$316.00" [ref=e569]:
- generic [ref=e570]: $316.00
- cell "Open menu" [ref=e571]:
- button "Open menu" [ref=e572]:
- generic [ref=e573]: Open menu
- img
- row "Select row success Abe45@example.com $242.00 Open menu" [ref=e578]:
- cell "Select row" [ref=e579]:
- checkbox "Select row" [ref=e580]
- cell "success" [ref=e581]:
- generic [ref=e582]: success
- cell "Abe45@example.com" [ref=e583]:
- generic [ref=e584]: Abe45@example.com
- cell "$242.00" [ref=e585]:
- generic [ref=e586]: $242.00
- cell "Open menu" [ref=e587]:
- button "Open menu" [ref=e588]:
- generic [ref=e589]: Open menu
- img
- row "Select row processing Monserrat44@example.com $837.00 Open menu" [ref=e594]:
- cell "Select row" [ref=e595]:
- checkbox "Select row" [ref=e596]
- cell "processing" [ref=e597]:
- generic [ref=e598]: processing
- cell "Monserrat44@example.com" [ref=e599]:
- generic [ref=e600]: Monserrat44@example.com
- cell "$837.00" [ref=e601]:
- generic [ref=e602]: $837.00
- cell "Open menu" [ref=e603]:
- button "Open menu" [ref=e604]:
- generic [ref=e605]: Open menu
- img
- row "Select row failed carmella@example.com $721.00 Open menu" [ref=e610]:
- cell "Select row" [ref=e611]:
- checkbox "Select row" [ref=e612]
- cell "failed" [ref=e613]:
- generic [ref=e614]: failed
- cell "carmella@example.com" [ref=e615]:
- generic [ref=e616]: carmella@example.com
- cell "$721.00" [ref=e617]:
- generic [ref=e618]: $721.00
- cell "Open menu" [ref=e619]:
- button "Open menu" [ref=e620]:
- generic [ref=e621]: Open menu
- img
- row "Select row pending jason78@example.com $450.00 Open menu" [ref=e626]:
- cell "Select row" [ref=e627]:
- checkbox "Select row" [ref=e628]
- cell "pending" [ref=e629]:
- generic [ref=e630]: pending
- cell "jason78@example.com" [ref=e631]:
- generic [ref=e632]: jason78@example.com
- cell "$450.00" [ref=e633]:
- generic [ref=e634]: $450.00
- cell "Open menu" [ref=e635]:
- button "Open menu" [ref=e636]:
- generic [ref=e637]: Open menu
- img
- row "Select row success sarah23@example.com $1,280.00 Open menu" [ref=e642]:
- cell "Select row" [ref=e643]:
- checkbox "Select row" [ref=e644]
- cell "success" [ref=e645]:
- generic [ref=e646]: success
- cell "sarah23@example.com" [ref=e647]:
- generic [ref=e648]: sarah23@example.com
- cell "$1,280.00" [ref=e649]:
- generic [ref=e650]: $1,280.00
- cell "Open menu" [ref=e651]:
- button "Open menu" [ref=e652]:
- generic [ref=e653]: Open menu
- img
- generic [ref=e658]:
- generic [ref=e659]: 0 of 6 row(s) selected.
- generic [ref=e660]:
- button "Previous" [disabled]
- button "Next" [disabled]
- generic [ref=e663]:
- generic [ref=e664]:
- generic [ref=e665]: Share this document
- generic [ref=e666]: Anyone with the link can view this document.
- generic [ref=e667]:
- generic [ref=e668]:
- generic [ref=e669]: Link
- textbox "Link" [ref=e670]: http://example.com/link/to/document
- button "Copy Link" [ref=e671]
- generic [ref=e672]:
- generic [ref=e673]: People with access
- generic [ref=e674]:
- generic [ref=e675]:
- generic [ref=e676]:
- img "Image" [ref=e678]
- generic [ref=e679]:
- paragraph [ref=e680]: Olivia Martin
- paragraph [ref=e681]: m@example.com
- combobox "Edit" [ref=e682]:
- generic: Can edit
- img
- generic [ref=e686]:
- generic [ref=e687]:
- img "Image" [ref=e689]
- generic [ref=e690]:
- paragraph [ref=e691]: Isabella Nguyen
- paragraph [ref=e692]: b@example.com
- combobox "Edit" [ref=e693]:
- generic: Can edit
- img
- generic [ref=e697]:
- generic [ref=e698]:
- img "Image" [ref=e700]
- generic [ref=e701]:
- paragraph [ref=e702]: Sofia Davis
- paragraph [ref=e703]: p@example.com
- combobox "Edit" [ref=e704]:
- generic: Can edit
- img
- generic [ref=e708]:
- generic [ref=e709]:
- img "Image" [ref=e711]
- generic [ref=e712]:
- paragraph [ref=e713]: Ethan Thompson
- paragraph [ref=e714]: e@example.com
- combobox "Edit" [ref=e715]:
- generic: Can edit
- img
- contentinfo [ref=e719]:
- generic [ref=e722]:
- img
- checkbox [checked]
- generic [ref=e284]: Social Media
- group [ref=e286]:
- checkbox "Search Engine" [ref=e287]
- checkbox
- generic [ref=e288]: Search Engine
- group [ref=e290]:
- checkbox "Referral" [ref=e291]
- checkbox
- generic [ref=e292]: Referral
- group [ref=e294]:
- checkbox "Other" [ref=e295]
- checkbox
- generic [ref=e296]: Other
- generic [ref=e297]:
- generic [ref=e298]:
- generic [ref=e299]:
- status "Loading"
- generic [ref=e300]: Processing your request
- generic [ref=e301]: Please wait while we process your request. Do not refresh the page.
- button "Cancel" [ref=e303]
- contentinfo [ref=e304]:
- generic [ref=e307]:
- text: Built by
- link "shadcn" [ref=e723] [cursor=pointer]:
- link "shadcn" [ref=e308] [cursor=pointer]:
- /url: https://twitter.com/shadcn
- text: at
- link "Vercel" [ref=e724] [cursor=pointer]:
- link "Vercel" [ref=e309] [cursor=pointer]:
- /url: https://vercel.com/new?utm_source=shadcn_site&utm_medium=web&utm_campaign=docs_cta_deploy_now_callout
- text: . The source code is available on
- link "GitHub" [ref=e725] [cursor=pointer]:
- link "GitHub" [ref=e310] [cursor=pointer]:
- /url: https://github.com/shadcn-ui/ui
- text: .
- region "Notifications alt+T"
- alert [ref=e728]
- generic [ref=e729]: Mon
- alert [ref=e311]