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 { createMCPClient } from './mcp-client.js'
import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest' import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' 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', () => { describe('MCP Server Tests', () => {
let client: Awaited<ReturnType<typeof createMCPClient>>['client'] let client: Awaited<ReturnType<typeof createMCPClient>>['client']
let cleanup: (() => Promise<void>) | null = null let cleanup: (() => Promise<void>) | null = null
beforeAll(async () => { beforeAll(async () => {
const result = await createMCPClient() await killProcessOnPort(9988)
client = result.client const result = await createMCPClient()
cleanup = result.cleanup 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: {},
}) })
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", "type": "text",
}, },
] ]
`) `)
expect(result.content).toBeDefined()
}, 30000)
// Navigate to a page and log something it('should get accessibility snapshot of hacker news', async () => {
const result = await client.callTool({ await client.callTool({
name: 'execute', name: 'execute',
arguments: { arguments: {
code: ` code: js`
await page.goto('https://news.ycombinator.com'); const newPage = await context.newPage();
await page.evaluate(() => { state.page = newPage;
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",
}, },
] })
`)
// Get console logs const result = await client.callTool({
const logsResult = (await client.callTool({ name: 'execute',
name: 'console_logs', arguments: {
arguments: { code: js`
limit: 10, await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' });
}, const snapshot = await state.page._snapshotForAI();
})) as CallToolResult return snapshot;
`,
expect(logsResult.content).toBeDefined()
expect(logsResult.content).toMatchInlineSnapshot(`
[
{
"text": "[log]: Test log message :1:32
[error]: Test error message :2:32",
"type": "text",
}, },
] })
`)
// Close the page opened const initialData =
await client.callTool({ typeof result === 'object' && result.content?.[0]?.text
name: 'close_page', ? tryJsonParse(result.content[0].text)
arguments: {}, : result
}) expect(initialData).toMatchFileSnapshot(
}, 30000) '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 () => { it('should get accessibility snapshot of shadcn UI', async () => {
// Create new page await client.callTool({
await client.callTool({ name: 'execute',
name: 'new_page', arguments: {
arguments: {}, code: js`
}) const newPage = await context.newPage();
state.page = newPage;
`,
},
})
// Navigate to a specific old Hacker News story that won't change const snapshot = await client.callTool({
await client.callTool({ name: 'execute',
name: 'execute', arguments: {
arguments: { code: js`
code: `await page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' })`, await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' });
}, const snapshot = await state.page._snapshotForAI();
}) return snapshot;
`,
},
})
// Get initial accessibility snapshot const data =
const initialSnapshot = await client.callTool({ typeof snapshot === 'object' && snapshot.content?.[0]?.text
name: 'accessibility_snapshot', ? tryJsonParse(snapshot.content[0].text)
arguments: {}, : snapshot
}) expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md')
expect(initialSnapshot.content).toBeDefined() expect(snapshot.content).toBeDefined()
expect(data).toContain('shadcn')
// Save initial snapshot }, 30000)
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)
}) })
function tryJsonParse(str: string) { function tryJsonParse(str: string) {
try { try {
return JSON.parse(str) return JSON.parse(str)
} catch { } catch {
return str return str
} }
} }
+66 -523
View File
@@ -1,239 +1,78 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod' 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 fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import os from 'node:os' import { spawn } from 'node:child_process'
import { spawn } from 'child_process' import { createRequire } from 'node:module'
import type { ChildProcess } from 'child_process' import vm from 'node:vm'
import { getBrowserExecutablePath } from './browser-config.js'
// Chrome executable finding logic moved to browser-config.ts const require = createRequire(import.meta.url)
// Store for maintaining state across tool calls
interface ToolState { interface ToolState {
isConnected: boolean isConnected: boolean
page: Page | null page: Page | null
browser: Browser | null browser: Browser | null
chromeProcess: ChildProcess | null
consoleLogs: Map<Page, ConsoleMessage[]>
networkRequests: Map<Page, NetworkRequest[]>
} }
const state: ToolState = { const state: ToolState = {
isConnected: false, isConnected: false,
page: null, page: null,
browser: null, browser: null,
chromeProcess: null,
consoleLogs: new Map(),
networkRequests: new Map(),
} }
interface ConsoleMessage { const RELAY_PORT = 9988
type: string
text: string
timestamp: number
location?: {
url: string
lineNumber: number
columnNumber: number
}
}
interface NetworkRequest { async function isPortTaken(port: number): Promise<boolean> {
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> {
try { try {
const response = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`) const response = await fetch(`http://localhost:${port}/`)
return response.ok return response.ok
} catch { } catch {
return false return false
} }
} }
// Launch Chrome with CDP enabled async function ensureRelayServer(): Promise<void> {
async function launchChromeWithCDP(): Promise<ChildProcess> { const portTaken = await isPortTaken(RELAY_PORT)
const userDataDir = path.join(os.homedir(), '.playwriter')
if (!fs.existsSync(userDataDir)) { if (portTaken) {
fs.mkdirSync(userDataDir, { recursive: true }) console.error('CDP relay server already running')
return
} }
const executablePath = getBrowserExecutablePath() console.error('Starting CDP relay server...')
const chromeArgs = [ const scriptPath = require.resolve('../dist/start-relay-server.js')
`--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 chromeProcess = spawn(executablePath, chromeArgs, { const serverProcess = spawn(process.execPath, [scriptPath], {
detached: true, detached: true,
stdio: 'ignore', stdio: 'ignore',
}) })
// Unref the process so it doesn't keep the parent process alive serverProcess.unref()
chromeProcess.unref()
// Give Chrome time to start up // wait for extension to connect
await new Promise((resolve) => setTimeout(resolve, 2000)) 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 }> { async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
if (state.isConnected && state.browser && state.page) { if (state.isConnected && state.browser && state.page) {
return { browser: state.browser, page: state.page } return { browser: state.browser, page: state.page }
} }
// Check if CDP is already available await ensureRelayServer()
const cdpAvailable = await isCDPAvailable()
if (!cdpAvailable) { const cdpEndpoint = `ws://localhost:${RELAY_PORT}/cdp/${Date.now()}`
// Launch Chrome with CDP const browser = await chromium.connectOverCDP(cdpEndpoint)
const chromeProcess = await launchChromeWithCDP()
state.chromeProcess = chromeProcess
}
// 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() 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() const pages = context.pages()
let page: Page const page = pages.length > 0 ? pages[0] : await context.newPage()
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
})
})
}
state.browser = browser state.browser = browser
state.page = page state.page = page
@@ -242,319 +81,30 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
return { browser, 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({ const server = new McpServer({
name: 'playwriter', name: 'playwriter',
title: 'Playwright MCP Server', title: 'Playwright MCP Server',
version: '1.0.0', 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') const promptContent = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'prompt.md'), 'utf-8')
server.tool( server.tool(
@@ -569,14 +119,15 @@ server.tool(
timeout: z.number().default(3000).describe('Timeout in milliseconds for code execution (default: 3000ms)'), timeout: z.number().default(3000).describe('Timeout in milliseconds for code execution (default: 3000ms)'),
}, },
async ({ code, timeout }) => { async ({ code, timeout }) => {
const { page } = await ensureConnection() await ensureConnection()
const page = getCurrentPage()
const context = page.context() const context = page.context()
console.error('Executing code:', code) console.error('Executing code:', code)
try { try {
// Collect console logs during execution
const consoleLogs: Array<{ method: string; args: any[] }> = [] const consoleLogs: Array<{ method: string; args: any[] }> = []
// Create a custom console object that collects logs
const customConsole = { const customConsole = {
log: (...args: any[]) => { log: (...args: any[]) => {
consoleLogs.push({ method: 'log', args }) consoleLogs.push({ method: 'log', args })
@@ -595,30 +146,27 @@ server.tool(
}, },
} }
// Create a function that has page, context, and console in scope const vmContext = vm.createContext({
const executeCode = new Function( page,
'page', context,
'context', state,
'console', console: customConsole,
` })
return (async () => {
${code} const wrappedCode = `(async () => { ${code} })()`
})();
`,
)
// Execute the code with page, context, and custom console with timeout
const result = await Promise.race([ const result = await Promise.race([
executeCode(page, context, customConsole), vm.runInContext(wrappedCode, vmContext, {
timeout,
displayErrors: true,
}),
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout), setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout),
), ),
]) ])
// Format the response with both console output and return value
let responseText = '' let responseText = ''
// Add console logs if any
if (consoleLogs.length > 0) { if (consoleLogs.length > 0) {
responseText += 'Console output:\n' responseText += 'Console output:\n'
consoleLogs.forEach(({ method, args }) => { consoleLogs.forEach(({ method, args }) => {
@@ -635,10 +183,13 @@ server.tool(
responseText += '\n' responseText += '\n'
} }
// Add return value if any
if (result !== undefined) { if (result !== undefined) {
responseText += 'Return value:\n' 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) { } else if (consoleLogs.length === 0) {
responseText += 'Code executed successfully (no output)' responseText += 'Code executed successfully (no output)'
} }
@@ -656,7 +207,7 @@ server.tool(
content: [ content: [
{ {
type: 'text', type: 'text',
text: `Error executing code: ${error.message}\n${error.stack}`, text: `Error executing code: ${error.message}\n${error.stack || ''}`,
}, },
], ],
isError: true, isError: true,
@@ -672,25 +223,17 @@ async function main() {
console.error('Playwright MCP server running on stdio') console.error('Playwright MCP server running on stdio')
} }
// Cleanup function
async function cleanup() { async function cleanup() {
console.error('Shutting down MCP server...') console.error('Shutting down MCP server...')
if (state.browser) { if (state.browser) {
try { 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() await state.browser.close()
} catch (e) { } catch (e) {
// Ignore errors during browser close // 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) process.exit(0)
} }
@@ -1,202 +1,318 @@
Return value:
- table [ref=e3]: - table [ref=e3]:
- rowgroup [ref=e4]: - rowgroup [ref=e4]:
- row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e5]: - row "Hacker Newsnew | threads | past | comments | ask | show | jobs | submit xmorse (70) | logout" [ref=e5]:
- cell "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e6]: - cell "Hacker Newsnew | threads | past | comments | ask | show | jobs | submit xmorse (70) | logout" [ref=e6]:
- table [ref=e7]: - table [ref=e7]:
- rowgroup [ref=e8]: - 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]: - cell [ref=e10]:
- link [ref=e11] [cursor=pointer]: - link [ref=e11] [cursor=pointer]:
- /url: https://news.ycombinator.com - /url: https://news.ycombinator.com
- img [ref=e12] [cursor=pointer] - img [ref=e12]
- cell "Hacker Newsnew | past | comments | ask | show | jobs | submit" [ref=e13]: - cell "Hacker Newsnew | threads | past | comments | ask | show | jobs | submit" [ref=e13]:
- generic [ref=e14]: - generic [ref=e14]:
- link "Hacker News" [ref=e16] [cursor=pointer]: - link "Hacker News" [ref=e16] [cursor=pointer]:
- /url: news - /url: news
- link "new" [ref=e17] [cursor=pointer]: - link "new" [ref=e17] [cursor=pointer]:
- /url: newest - /url: newest
- text: "|" - 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 - /url: front
- text: "|" - text: "|"
- link "comments" [ref=e19] [cursor=pointer]: - link "comments" [ref=e20] [cursor=pointer]:
- /url: newcomments - /url: newcomments
- text: "|" - text: "|"
- link "ask" [ref=e20] [cursor=pointer]: - link "ask" [ref=e21] [cursor=pointer]:
- /url: ask - /url: ask
- text: "|" - text: "|"
- link "show" [ref=e21] [cursor=pointer]: - link "show" [ref=e22] [cursor=pointer]:
- /url: show - /url: show
- text: "|" - text: "|"
- link "jobs" [ref=e22] [cursor=pointer]: - link "jobs" [ref=e23] [cursor=pointer]:
- /url: jobs - /url: jobs
- text: "|" - text: "|"
- link "submit" [ref=e23] [cursor=pointer]: - link "submit" [ref=e24] [cursor=pointer]:
- /url: submit - /url: submit
- cell "login" [ref=e24]: - cell "xmorse (70) | logout" [ref=e25]:
- link "login" [ref=e26] [cursor=pointer]: - generic [ref=e26]:
- /url: login?goto=item%3Fid%3D1 - link "xmorse" [ref=e27] [cursor=pointer]:
- row [ref=e27] - /url: user?id=xmorse
- 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]: - text: (70) |
- 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]: - link "logout" [ref=e28] [cursor=pointer]:
- table [ref=e30]: - /url: logout?auth=227f860372905c327e92615af914e1e41ec43fff&goto=item%3Fid%3D1
- rowgroup [ref=e31]: - row [ref=e29]
- row "upvote Y Combinator (ycombinator.com)" [ref=e32]: - row [ref=e30]:
- cell [ref=e33] - cell [ref=e31]:
- cell "upvote" [ref=e35]: - table [ref=e32]:
- link "upvote" [ref=e37] [cursor=pointer]: - rowgroup [ref=e33]:
- /url: vote?id=1&how=up&goto=item%3Fid%3D1 - row "upvote Y Combinator (ycombinator.com)" [ref=e34]:
- cell "Y Combinator (ycombinator.com)" [ref=e39]: - cell [ref=e35]
- generic [ref=e40]: - cell "upvote" [ref=e36]:
- link "Y Combinator" [ref=e41] [cursor=pointer]: - 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 - /url: http://ycombinator.com
- generic [ref=e42]: - generic [ref=e43]:
- text: ( - text: (
- link "ycombinator.com" [ref=e43] [cursor=pointer]: - link "ycombinator.com" [ref=e44] [cursor=pointer]:
- /url: from?site=ycombinator.com - /url: from?site=ycombinator.com
- generic [ref=e44] [cursor=pointer]: ycombinator.com
- text: ) - 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 [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=e48]:
- generic [ref=e49]: 57 points - text: 57 points by
- text: by - link "pg" [ref=e49] [cursor=pointer]:
- link "pg" [ref=e50] [cursor=pointer]:
- /url: user?id=pg - /url: user?id=pg
- link "on Oct 9, 2006" [ref=e52] [cursor=pointer]: - generic "2006-10-09T18:21:51 1160418111" [ref=e50]:
- /url: item?id=1 - link "on Oct 9, 2006" [ref=e51] [cursor=pointer]:
- /url: item?id=1
- text: "|" - text: "|"
- link "hide" [ref=e54] [cursor=pointer]: - link "hide" [ref=e52] [cursor=pointer]:
- /url: hide?id=1&goto=item%3Fid%3D1 - /url: hide?id=1&auth=e52b391ec392eb381ace51126fd80e19d48c1d7b&goto=item%3Fid%3D1
- text: "|" - 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 - /url: https://hn.algolia.com/?query=Y%20Combinator&type=story&dateRange=all&sort=byDate&storyText=false&prefix&page=0
- text: "|" - text: "|"
- link "favorite" [ref=e56] [cursor=pointer]: - link "favorite" [ref=e54] [cursor=pointer]:
- /url: fave?id=1&auth=5328fcfde7333d68e67a8a2334e25acee5599932 - /url: fave?id=1&auth=e52b391ec392eb381ace51126fd80e19d48c1d7b
- text: "|" - text: "|"
- link "3 comments" [ref=e57] [cursor=pointer]: - link "16 comments" [ref=e55] [cursor=pointer]:
- /url: item?id=1 - /url: item?id=1
- row [ref=e58]: - row [ref=e56]:
- cell [ref=e59] - cell [ref=e57]
- cell [ref=e60] - cell [ref=e58]
- table [ref=e63]: - table [ref=e59]:
- rowgroup [ref=e64]: - rowgroup [ref=e60]:
- row "upvote sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e65]: - 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=e66]: - cell "upvote sama on Oct 9, 2006 | [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e62]:
- table [ref=e67]: - table [ref=e63]:
- rowgroup [ref=e68]: - rowgroup [ref=e64]:
- row "upvote sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e69]: - row "upvote sama on Oct 9, 2006 | [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e65]:
- cell [ref=e70]: - cell [ref=e66]:
- img - img
- cell "upvote" [ref=e72]: - cell "upvote" [ref=e67]:
- link "upvote" [ref=e74] [cursor=pointer]: - link "upvote" [ref=e69] [cursor=pointer]:
- /url: vote?id=15&how=up&goto=item%3Fid%3D1 - /url: vote?id=15&how=up&auth=709295bd69cc8638fcf14a7b807bb0efabd69a2d&goto=item%3Fid%3D1#15
- cell "sama on Oct 9, 2006 [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e76]: - generic "upvote" [ref=e70]
- generic [ref=e78]: - cell "sama on Oct 9, 2006 | [] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e71]:
- link "sama" [ref=e79] [cursor=pointer]: - generic [ref=e73]:
- link "sama" [ref=e74] [cursor=pointer]:
- /url: user?id=sama - /url: user?id=sama
- link "on Oct 9, 2006" [ref=e81] [cursor=pointer]: - generic "2006-10-09T19:51:01 1160423461" [ref=e75]:
- /url: item?id=15 - link "on Oct 9, 2006" [ref=e76] [cursor=pointer]:
- link "[]" [ref=e84] [cursor=pointer]: - /url: item?id=15
- /url: javascript:void(0) - generic [ref=e77]:
- generic [ref=e87]: - text: "|"
- generic [ref=e88]: "\"the rising star of venture capital\" -unknown VC eating lunch on SHR" - 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: - generic:
- paragraph - paragraph
- row "upvote pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road?" [ref=e92]: - 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=e93]: - cell "upvote pg on Oct 9, 2006 | | [] Is there anywhere to eat on Sandhill Road?" [ref=e83]:
- table [ref=e94]: - table [ref=e84]:
- rowgroup [ref=e95]: - rowgroup [ref=e85]:
- row "upvote pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road?" [ref=e96]: - row "upvote pg on Oct 9, 2006 | | [] Is there anywhere to eat on Sandhill Road?" [ref=e86]:
- cell [ref=e97]: - cell [ref=e87]:
- img [ref=e98] - img [ref=e88]
- cell "upvote" [ref=e99]: - cell "upvote" [ref=e89]:
- link "upvote" [ref=e101] [cursor=pointer]: - link "upvote" [ref=e91] [cursor=pointer]:
- /url: vote?id=17&how=up&goto=item%3Fid%3D1 - /url: vote?id=17&how=up&auth=3c24b126f8f812f8c3a24a0b9143eac42ca15476&goto=item%3Fid%3D1#17
- cell "pg on Oct 9, 2006 | [] Is there anywhere to eat on Sandhill Road?" [ref=e103]: - generic "upvote" [ref=e92]
- generic [ref=e105]: - cell "pg on Oct 9, 2006 | | [] Is there anywhere to eat on Sandhill Road?" [ref=e93]:
- link "pg" [ref=e106] [cursor=pointer]: - generic [ref=e95]:
- link "pg" [ref=e96] [cursor=pointer]:
- /url: user?id=pg - /url: user?id=pg
- link "on Oct 9, 2006" [ref=e108] [cursor=pointer]: - generic "2006-10-09T19:52:45 1160423565" [ref=e97]:
- /url: item?id=17 - link "on Oct 9, 2006" [ref=e98] [cursor=pointer]:
- generic [ref=e110]: - /url: item?id=17
- generic [ref=e99]:
- text: "|" - text: "|"
- link [ref=e111] [cursor=pointer]: - link [ref=e100] [cursor=pointer]:
- /url: "#15" - /url: "#15"
- text: parent - 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) - /url: javascript:void(0)
- generic [ref=e115]: - generic [ref=e103]:
- generic [ref=e116]: Is there anywhere to eat on Sandhill Road? - generic [ref=e104]: Is there anywhere to eat on Sandhill Road?
- generic: - generic:
- paragraph - paragraph
- row "upvote dmon on Feb 25, 2007 | | [] sure" [ref=e120]: - row "upvote dmon on Feb 25, 2007 | | | [] sure" [ref=e105]:
- cell "upvote dmon on Feb 25, 2007 | | [] sure" [ref=e121]: - cell "upvote dmon on Feb 25, 2007 | | | [] sure" [ref=e106]:
- table [ref=e122]: - table [ref=e107]:
- rowgroup [ref=e123]: - rowgroup [ref=e108]:
- row "upvote dmon on Feb 25, 2007 | | [] sure" [ref=e124]: - row "upvote dmon on Feb 25, 2007 | | | [] sure" [ref=e109]:
- cell [ref=e125]: - cell [ref=e110]:
- img [ref=e126] - img [ref=e111]
- cell "upvote" [ref=e127]: - cell "upvote" [ref=e112]:
- link "upvote" [ref=e129] [cursor=pointer]: - link "upvote" [ref=e114] [cursor=pointer]:
- /url: vote?id=1079&how=up&goto=item%3Fid%3D1 - /url: vote?id=1079&how=up&auth=851f0b1e178c46300e9d2218764827726415a50d&goto=item%3Fid%3D1#1079
- cell "dmon on Feb 25, 2007 | | [] sure" [ref=e131]: - generic "upvote" [ref=e115]
- generic [ref=e133]: - cell "dmon on Feb 25, 2007 | | | [] sure" [ref=e116]:
- link "dmon" [ref=e134] [cursor=pointer]: - generic [ref=e118]:
- link "dmon" [ref=e119] [cursor=pointer]:
- /url: user?id=dmon - /url: user?id=dmon
- link "on Feb 25, 2007" [ref=e136] [cursor=pointer]: - generic "2007-02-25T22:18:23 1172441903" [ref=e120]:
- /url: item?id=1079 - link "on Feb 25, 2007" [ref=e121] [cursor=pointer]:
- generic [ref=e138]: - /url: item?id=1079
- generic [ref=e122]:
- text: "|" - text: "|"
- link [ref=e139] [cursor=pointer]: - link [ref=e123] [cursor=pointer]:
- /url: "#15" - /url: "#15"
- text: root - text: root
- text: "|" - text: "|"
- link [ref=e140] [cursor=pointer]: - link [ref=e124] [cursor=pointer]:
- /url: "#17" - /url: "#17"
- text: parent - 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) - /url: javascript:void(0)
- generic [ref=e144]: - generic [ref=e127]:
- generic [ref=e145]: sure - generic [ref=e128]: sure
- generic: - generic:
- paragraph - 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]: - 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 "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]: - 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 - img
- table [ref=e154]: - table [ref=e191]:
- rowgroup [ref=e155]: - rowgroup [ref=e192]:
- row [ref=e156]: - row [ref=e193]:
- cell [ref=e157] - cell [ref=e194]
- link "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4" [ref=e160] [cursor=pointer]: - generic [ref=e195]:
- /url: https://www.ycombinator.com/apply/ - generic [ref=e196]:
- generic [ref=e162]: - link "Guidelines" [ref=e197] [cursor=pointer]:
- generic [ref=e163]:
- link "Guidelines" [ref=e164] [cursor=pointer]:
- /url: newsguidelines.html - /url: newsguidelines.html
- text: "|" - text: "|"
- link "FAQ" [ref=e165] [cursor=pointer]: - link "FAQ" [ref=e198] [cursor=pointer]:
- /url: newsfaq.html - /url: newsfaq.html
- text: "|" - text: "|"
- link "Lists" [ref=e166] [cursor=pointer]: - link "Lists" [ref=e199] [cursor=pointer]:
- /url: lists - /url: lists
- text: "|" - text: "|"
- link "API" [ref=e167] [cursor=pointer]: - link "API" [ref=e200] [cursor=pointer]:
- /url: https://github.com/HackerNews/API - /url: https://github.com/HackerNews/API
- text: "|" - text: "|"
- link "Security" [ref=e168] [cursor=pointer]: - link "Security" [ref=e201] [cursor=pointer]:
- /url: security.html - /url: security.html
- text: "|" - text: "|"
- link "Legal" [ref=e169] [cursor=pointer]: - link "Legal" [ref=e202] [cursor=pointer]:
- /url: https://www.ycombinator.com/legal/ - /url: https://www.ycombinator.com/legal/
- text: "|" - text: "|"
- link "Apply to YC" [ref=e170] [cursor=pointer]: - link "Apply to YC" [ref=e203] [cursor=pointer]:
- /url: https://www.ycombinator.com/apply/ - /url: https://www.ycombinator.com/apply/
- text: "|" - text: "|"
- link "Contact" [ref=e171] [cursor=pointer]: - link "Contact" [ref=e204] [cursor=pointer]:
- /url: mailto:hn@ycombinator.com - /url: mailto:hn@ycombinator.com
- generic [ref=e174]: - generic [ref=e205]:
- text: "Search:" - text: "Search:"
- textbox [ref=e175] - textbox [ref=e206]
@@ -1,3 +1,4 @@
Return value:
- generic [active] [ref=e1]: - generic [active] [ref=e1]:
- generic [ref=e2]: - generic [ref=e2]:
- banner [ref=e3]: - banner [ref=e3]:
@@ -5,549 +6,338 @@
- link "shadcn/ui" [ref=e6] [cursor=pointer]: - link "shadcn/ui" [ref=e6] [cursor=pointer]:
- /url: / - /url: /
- img - img
- generic [ref=e11] [cursor=pointer]: shadcn/ui - generic [ref=e7]: shadcn/ui
- navigation [ref=e12]: - navigation [ref=e8]:
- link "Docs" [ref=e13] [cursor=pointer]: - link "Docs" [ref=e9] [cursor=pointer]:
- /url: /docs/installation - /url: /docs/installation
- link "Components" [ref=e14] [cursor=pointer]: - link "Components" [ref=e10] [cursor=pointer]:
- /url: /docs/components - /url: /docs/components
- link "Blocks" [ref=e15] [cursor=pointer]: - link "Blocks" [ref=e11] [cursor=pointer]:
- /url: /blocks - /url: /blocks
- link "Charts" [ref=e16] [cursor=pointer]: - link "Charts" [ref=e12] [cursor=pointer]:
- /url: /charts/area - /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 - /url: /themes
- link "Colors" [ref=e18] [cursor=pointer]: - link "Colors" [ref=e15] [cursor=pointer]:
- /url: /colors - /url: /colors
- generic [ref=e19]: - generic [ref=e16]:
- button "Search documentation... ⌘ K" [ref=e21]: - button "Search documentation... ⌘ K" [ref=e18]:
- generic [ref=e22]: Search documentation... - generic [ref=e19]: Search documentation...
- generic [ref=e23]: - generic [ref=e21]:
- generic: ⌘ - generic: ⌘
- generic: K - generic: K
- link "91.7k" [ref=e26] [cursor=pointer]: - link "98.7k" [ref=e22] [cursor=pointer]:
- /url: https://github.com/shadcn-ui/ui - /url: https://github.com/shadcn-ui/ui
- img - img
- generic [ref=e29] [cursor=pointer]: 91.7k - generic [ref=e23]: 98.7k
- button "Toggle layout" [ref=e30]: - button "Toggle layout" [ref=e24]:
- generic [ref=e31]: Toggle layout - generic [ref=e25]: Toggle layout
- img - img
- button "Toggle theme" [ref=e36]: - button "Toggle theme" [ref=e26]:
- img - img
- generic [ref=e44]: Toggle theme - generic [ref=e27]: Toggle theme
- main [ref=e45]: - main [ref=e28]:
- generic [ref=e46]: - generic [ref=e29]:
- generic [ref=e49]: - generic [ref=e32]:
- link "New Calendar Component" [ref=e50] [cursor=pointer]: - 'link "New New Components: Field, Input Group, Item and more" [ref=e33] [cursor=pointer]':
- /url: /docs/components/calendar - /url: /docs/changelog
- text: New Calendar Component - generic "New" [ref=e34]
- text: "New Components: Field, Input Group, Item and more"
- img - img
- heading "The Foundation for your Design System" [level=1] [ref=e54] - heading "The Foundation for your Design System" [level=1] [ref=e35]
- 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. - 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=e56]: - generic [ref=e37]:
- link "Get Started" [ref=e57] [cursor=pointer]: - link "Get Started" [ref=e38] [cursor=pointer]:
- /url: /docs/installation - /url: /docs/installation
- link "View Components" [ref=e58] [cursor=pointer]: - link "View Components" [ref=e39] [cursor=pointer]:
- /url: /docs/components - /url: /docs/components
- generic [ref=e60]: - generic [ref=e41]:
- generic [ref=e65]: - generic [ref=e46]:
- link "Examples" [ref=e66] [cursor=pointer]: - link "Examples" [ref=e47] [cursor=pointer]:
- /url: / - /url: /
- link "Dashboard" [ref=e67] [cursor=pointer]: - link "Dashboard" [ref=e48] [cursor=pointer]:
- /url: /examples/dashboard - /url: /examples/dashboard
- link "Tasks" [ref=e68] [cursor=pointer]: - link "Tasks" [ref=e49] [cursor=pointer]:
- /url: /examples/tasks - /url: /examples/tasks
- link "Playground" [ref=e69] [cursor=pointer]: - link "Playground" [ref=e50] [cursor=pointer]:
- /url: /examples/playground - /url: /examples/playground
- link "Authentication" [ref=e70] [cursor=pointer]: - link "Authentication" [ref=e51] [cursor=pointer]:
- /url: /examples/authentication - /url: /examples/authentication
- generic [ref=e71]: - generic [ref=e52]:
- generic [ref=e72]: Theme - generic [ref=e53]: Theme
- combobox "Theme" [ref=e73]: - combobox "Theme" [ref=e54]:
- generic [ref=e74]: "Theme:" - generic [ref=e55]: "Theme:"
- generic: Default - generic: Neutral
- img - img
- generic [ref=e81]: - button "Copy Code" [ref=e56]:
- generic [ref=e82]: - img
- generic [ref=e83]: - generic [ref=e57]: Copy Code
- generic [ref=e84]: - generic [ref=e61]:
- generic [ref=e85]: - generic [ref=e65]:
- generic [ref=e86]: Total Revenue - group "Payment Method" [ref=e66]:
- generic [ref=e87]: $15,231.89 - generic [ref=e67]: Payment Method
- generic [ref=e88]: +20.1% from last month - paragraph [ref=e68]: All transactions are secure and encrypted
- img [ref=e93] - generic [ref=e69]:
- generic [ref=e99]: - group [ref=e70]:
- generic [ref=e100]: - generic [ref=e71]: Name on Card
- generic [ref=e101]: Subscriptions - textbox "Name on Card" [ref=e72]:
- generic [ref=e102]: +2,350 - /placeholder: John Doe
- generic [ref=e103]: +180.1% from last month - generic [ref=e73]:
- button "View More" [ref=e105] - group [ref=e74]:
- img [ref=e110] - generic [ref=e75]: Card Number
- generic [ref=e123]: - textbox "Card Number" [ref=e76]:
- generic [ref=e124]: - /placeholder: 1234 5678 9012 3456
- generic [ref=e125]: - paragraph [ref=e77]: Enter your 16-digit number.
- generic [ref=e126]: - group [ref=e78]:
- generic [ref=e127]: Upgrade your subscription - generic [ref=e79]: CVV
- generic [ref=e128]: You are currently on the free plan. Upgrade to the pro plan to get access to all features. - textbox "CVV" [ref=e80]:
- generic [ref=e130]: - /placeholder: "123"
- generic [ref=e131]: - generic [ref=e81]:
- generic [ref=e132]: - group [ref=e82]:
- generic [ref=e133]: Name - generic [ref=e83]: Month
- textbox "Name" [ref=e134] - combobox "Month" [ref=e84]:
- generic [ref=e135]: - generic: MM
- 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]:
- img - img
- generic [ref=e269]: New message - combobox [ref=e85]
- generic [ref=e271]: - group [ref=e86]:
- generic [ref=e272]: Hi, how can I help you today? - generic [ref=e87]: Year
- generic [ref=e273]: Hey, I'm having trouble with my account. - combobox "Year" [ref=e88]:
- generic [ref=e274]: What seems to be the problem? - generic: YYYY
- generic [ref=e275]: I can't log in.
- generic [ref=e277]:
- textbox "Type your message..." [ref=e278]
- button "Send" [disabled]:
- img - img
- generic: Send - combobox [ref=e89]
- generic [ref=e285]: - group "Billing Address" [ref=e91]:
- generic [ref=e286]: - generic [ref=e92]: Billing Address
- generic [ref=e287]: Report an issue - paragraph [ref=e93]: The billing address associated with your payment method
- generic [ref=e288]: What area are you having problems with? - group [ref=e95]:
- generic [ref=e289]: - checkbox "Same as shipping address" [checked] [ref=e96]:
- 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
- generic: - 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:
- generic: Thursday - img
- generic: - checkbox [checked]
- generic: - generic [ref=e284]: Social Media
- generic: - group [ref=e286]:
- generic: - checkbox "Search Engine" [ref=e287]
- generic: Today - checkbox
- generic: "390" - generic [ref=e288]: Search Engine
- generic: - group [ref=e290]:
- generic: - checkbox "Referral" [ref=e291]
- generic: - checkbox
- generic: Average - generic [ref=e292]: Referral
- generic: "278" - group [ref=e294]:
- generic [ref=e542]: - checkbox "Other" [ref=e295]
- generic [ref=e543]: - checkbox
- generic [ref=e544]: Payments - generic [ref=e296]: Other
- generic [ref=e545]: Manage your payments. - generic [ref=e297]:
- button "Add Payment" [ref=e547] - generic [ref=e298]:
- generic [ref=e548]: - generic [ref=e299]:
- table [ref=e551]: - status "Loading"
- rowgroup [ref=e552]: - generic [ref=e300]: Processing your request
- row "Select all Status Email Amount" [ref=e553]: - generic [ref=e301]: Please wait while we process your request. Do not refresh the page.
- cell "Select all" [ref=e554]: - button "Cancel" [ref=e303]
- checkbox "Select all" [ref=e555] - contentinfo [ref=e304]:
- cell "Status" [ref=e556] - generic [ref=e307]:
- 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]:
- text: Built by - text: Built by
- link "shadcn" [ref=e723] [cursor=pointer]: - link "shadcn" [ref=e308] [cursor=pointer]:
- /url: https://twitter.com/shadcn - /url: https://twitter.com/shadcn
- text: at - 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 - /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 - 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 - /url: https://github.com/shadcn-ui/ui
- text: . - text: .
- region "Notifications alt+T" - region "Notifications alt+T"
- alert [ref=e728] - alert [ref=e311]
- generic [ref=e729]: Mon