copy from holocron

This commit is contained in:
Tommy D. Rossi
2025-11-13 19:24:50 +01:00
parent 8e5d68a6f2
commit 650345f47a
15 changed files with 2961 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
# Changelog
## 0.0.3
### Patch Changes
- Replace CommonJS `require` with ESM `import` for user-agents module
## 2025-07-24 22:15
- Changed Chrome process stdio from 'ignore' to 'inherit' to print Chrome logs
- Helps with debugging CDP connection issues
## 2025-07-24 22:00
- Simplified email validation by checking profiles directly in MCP connect tool
- Connect tool validates email against available profiles before starting Chrome
- Returns helpful message with available profiles when email doesn't match
- startPlaywriter now simply throws an error for invalid emails
## 2025-07-24 21:45
- Added test infrastructure with vitest for MCP server testing
- Created mcp-client.ts with MCP client setup using vite-node
- Added comprehensive tests for Chrome CDP connection and console log capture
- Fixed callTool signatures to match MCP SDK API
- Added proper TypeScript types for CallToolResult
## 2025-07-24 21:30
- Moved profile listing functionality into connect tool when emailProfile is not provided
- Updated parameter description with agent-appropriate phrasing ("ask your user/owner")
- Removed separate get_profiles tool for cleaner API
- Connect tool now handles both profile listing and connection in one place
## 2025-07-24 21:15
- Modified startPlaywriter to accept optional emailProfile parameter
- Removed prompts dependency and interactive profile selection
- Connect tool now accepts emailProfile parameter or returns available profiles
- Added security guidance for profile selection in MCP response
- Suggests storing selected email in AGENTS.md or CLAUDE.md to avoid repeated selection
## 2025-07-24 21:00
- Integrated Chrome launch via startPlaywriter from playwriter.ts
- Connect tool now starts Chrome with CDP port and connects via playwright.chromium.connectOverCDP
- Added proper cleanup handlers for browser and Chrome process on server shutdown
- Removed placeholder getActivePage function in favor of direct browser connection
## 2025-07-24 20:50
- Moved console object definition outside of the Function constructor template string
- Improved code readability and maintainability
## 2025-07-24 20:45
- Refactored console capture to use a custom console object instead of overriding global console
- Cleaner implementation that avoids modifying global state
## 2025-07-24 20:40
- Enhanced execute tool to capture console.log, console.info, console.warn, console.error, and console.debug output
- Console methods are temporarily overridden during code execution to collect logs
- Output now includes both console logs and return values in a formatted response
## 2025-07-24 20:35
- Added execute tool to run arbitrary JavaScript code with page and context in scope
- The tool uses the Playwright automation guide from prompt.md as its description
## 2025-07-24 20:30
- Fixed MCP server tool registration API usage to match the correct method signature (name, description, schema, handler)
+60
View File
@@ -0,0 +1,60 @@
# PlayWriter MCP
A powerful browser automation MCP (Model Context Protocol) server that provides persistent browser sessions with advanced automation capabilities through Playwright.
## Key Differences from playwright-mcp
### 🔒 Persistent Data Directory
PlayWriter MCP maintains a persistent user data directory between runs. This enables:
- Login sessions with Google and other OAuth providers
- Website authentication that persists across sessions
- Test automation without repeated logins
- Preserved cookies, local storage, and browser state
### 🛡️ Anti-Detection Features
Built-in detection prevention mechanisms allow automation on websites with bot protection:
- Works seamlessly with Google and other major platforms
- User-agent rotation
- Automation flag removal
- Realistic browser fingerprinting
### 🌐 Shared Chrome Instance
- Single Chrome instance shared between all agents
- Each agent operates in its own tab
- Can reuse existing pages/tabs via `context.pages()` in the execute tool
- Find and switch between pages by URL or other criteria
- Efficient resource usage
- Better performance for multi-agent workflows
### 🚀 Single Powerful Execute Tool
Instead of many granular tools, PlayWriter provides one flexible `execute` tool:
- Direct access to Playwright `page` and `context` objects
- Write complex automation logic with loops and conditions
- Full JavaScript execution capabilities
- Custom wait conditions and complex interactions
Example:
```javascript
// Complex wait logic with custom conditions
while (!(await page.isVisible('.success-message'))) {
await page.click('.retry-button');
await page.waitForTimeout(1000);
}
```
### 🔧 Additional Tools
- `new_page` - Create a new browser tab/page
- `accessibility_snapshot` - Get page structure as JSON
- `console_logs` - Retrieve browser console messages
- `network_history` - Monitor network requests
## Roadmap
- ☁️ Cloud service integration for shared persistent state
- 🧪 Quality assertion tests in CI/CD pipelines
- 🤖 Multi-agent collaboration features
- 📊 Advanced debugging and monitoring capabilities
## Installation
See the main project documentation for installation and setup instructions.
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
import './dist/mcp.js'
+38
View File
@@ -0,0 +1,38 @@
{
"name": "playwriter",
"description": "",
"version": "0.0.3",
"type": "module",
"repository": "https://github.com/remorses/",
"scripts": {
"build": "rm -rf dist *.tsbuildinfo && mkdir dist && cp src/prompt.md dist/ && tsc",
"prepublishOnly": "pnpm build",
"watch": "tsc -w",
"typecheck": "tsc --noEmit",
"mcp": "vite-node src/mcp.ts",
"test": "vitest run",
"test:watch": "vitest"
},
"bin": "./bin.js",
"files": [
"dist",
"src",
"esm",
"bin.js"
],
"keywords": [],
"author": "Tommaso De Rossi, morse <beats.by.morse@gmail.com>",
"license": "",
"devDependencies": {
"@vitest/ui": "^3.2.4",
"vite-node": "^3.2.4",
"vitest": "^3.2.4"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
"@playwright/mcp": "latest",
"patchright-core": "^1.52.5",
"user-agents": "^1.1.610",
"zod": "^4.1.12"
}
}
+64
View File
@@ -0,0 +1,64 @@
import fs from 'node:fs'
import os from 'node:os'
// Function to get the browser executable path
// Can be overridden by environment variable PLAYWRITER_BROWSER_PATH
export function getBrowserExecutablePath(): string {
// Check environment variable first
const envPath = process.env.PLAYWRITER_BROWSER_PATH
if (envPath && fs.existsSync(envPath)) {
return envPath
}
// Check for Ghost Browser on macOS
// const ghostBrowserPath = '/Applications/Ghost Browser.app/Contents/MacOS/Ghost Browser'
// if (fs.existsSync(ghostBrowserPath)) {
// return ghostBrowserPath
// }
// Fall back to finding Chrome
return findChromeExecutablePath()
}
// Original Chrome finding logic
function findChromeExecutablePath(): string {
const osPlatform = os.platform()
const paths = (() => {
if (osPlatform === 'darwin') {
return [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
]
}
if (osPlatform === 'win32') {
return [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
`${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`,
`${process.env['PROGRAMFILES(X86)']}\\Google\\Chrome\\Application\\chrome.exe`,
].filter(Boolean)
}
// Linux
return [
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/snap/bin/chromium',
]
})()
for (const path of paths) {
const resolvedPath = path.startsWith('~') ? path.replace('~', process.env.HOME || '') : path
if (fs.existsSync(resolvedPath)) {
return resolvedPath
}
}
throw new Error(
'Could not find Chrome executable. Please install Google Chrome or set PLAYWRITER_BROWSER_PATH environment variable.',
)
}
+73
View File
@@ -0,0 +1,73 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { Stream } from 'node:stream'
import path from 'node:path'
import url from 'node:url'
const __filename = url.fileURLToPath(import.meta.url)
export interface CreateTransportOptions {
clientName?: string
}
export async function createTransport(args: string[]): Promise<{
transport: Transport
stderr: Stream | null
}> {
const transport = new StdioClientTransport({
command: 'pnpm',
args: ['vite-node', path.join(path.dirname(__filename), 'mcp.ts'), ...args],
cwd: path.join(path.dirname(__filename), '..'),
stderr: 'pipe',
env: {
...process.env,
DEBUG: 'playwriter:mcp:test',
DEBUG_COLORS: '0',
DEBUG_HIDE_DATE: '1',
},
})
return {
transport,
stderr: transport.stderr!,
}
}
export async function createMCPClient(options?: CreateTransportOptions): Promise<{
client: Client
stderr: string
cleanup: () => Promise<void>
}> {
const client = new Client({
name: options?.clientName ?? 'test',
version: '1.0.0',
})
const { transport, stderr } = await createTransport([])
let stderrBuffer = ''
stderr?.on('data', (data) => {
process.stderr.write(data)
stderrBuffer += data.toString()
})
await client.connect(transport)
await client.ping()
const cleanup = async () => {
try {
await client.close()
} catch (e) {
console.error('Error during MCP client cleanup:', e)
// Ignore errors during cleanup
}
}
return {
client,
stderr: stderrBuffer,
cleanup,
}
}
+229
View File
@@ -0,0 +1,229 @@
import { createMCPClient } from './mcp-client.js'
import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
describe('MCP Server Tests', () => {
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: {},
})
expect(connectResult.content).toBeDefined()
expect(connectResult.content).toMatchInlineSnapshot(`
[
{
"text": "Created new page. URL: about:blank. Total pages: 20",
"type": "text",
},
]
`)
// 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",
},
]
`)
// 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",
},
]
`)
// Close the page opened
await client.callTool({
name: 'close_page',
arguments: {},
})
}, 30000)
it('should capture accessibility snapshot of hacker news', async () => {
// Create new page
await client.callTool({
name: 'new_page',
arguments: {},
})
// 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' })`,
},
})
// 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)
})
function tryJsonParse(str: string) {
try {
return JSON.parse(str)
} catch {
return str
}
}
+704
View File
@@ -0,0 +1,704 @@
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 'patchright-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'
// Chrome executable finding logic moved to browser-config.ts
// 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
}
}
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> {
try {
const response = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`)
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 })
}
const executablePath = getBrowserExecutablePath()
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 chromeProcess = spawn(executablePath, chromeArgs, {
detached: true,
stdio: 'ignore',
})
// Unref the process so it doesn't keep the parent process alive
chromeProcess.unref()
// Give Chrome time to start up
await new Promise((resolve) => setTimeout(resolve, 2000))
return chromeProcess
}
// 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()
if (!cdpAvailable) {
// Launch Chrome with CDP
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()
let context: BrowserContext
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
})
})
}
state.browser = browser
state.page = page
state.isConnected = true
return { browser, page }
}
// Initialize MCP server
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(
'execute',
promptContent,
{
code: z
.string()
.describe(
'JavaScript code to execute with page and context in scope. Should be one line, using ; to execute multiple statements. To execute complex actions call execute multiple times. ',
),
timeout: z.number().default(3000).describe('Timeout in milliseconds for code execution (default: 3000ms)'),
},
async ({ code, timeout }) => {
const { page } = await ensureConnection()
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 })
},
info: (...args: any[]) => {
consoleLogs.push({ method: 'info', args })
},
warn: (...args: any[]) => {
consoleLogs.push({ method: 'warn', args })
},
error: (...args: any[]) => {
consoleLogs.push({ method: 'error', args })
},
debug: (...args: any[]) => {
consoleLogs.push({ method: 'debug', args })
},
}
// Create a function that has page, context, and console in scope
const executeCode = new Function(
'page',
'context',
'console',
`
return (async () => {
${code}
})();
`,
)
// Execute the code with page, context, and custom console with timeout
const result = await Promise.race([
executeCode(page, context, customConsole),
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 }) => {
const formattedArgs = args
.map((arg) => {
if (typeof arg === 'object') {
return JSON.stringify(arg, null, 2)
}
return String(arg)
})
.join(' ')
responseText += `[${method}] ${formattedArgs}\n`
})
responseText += '\n'
}
// Add return value if any
if (result !== undefined) {
responseText += 'Return value:\n'
responseText += JSON.stringify(result, null, 2)
} else if (consoleLogs.length === 0) {
responseText += 'Code executed successfully (no output)'
}
return {
content: [
{
type: 'text',
text: responseText.trim(),
},
],
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error executing code: ${error.message}\n${error.stack}`,
},
],
isError: true,
}
}
},
)
// Start the server
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
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)
}
// Handle process termination
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
process.on('exit', () => {
// Browser cleanup is handled by the async cleanup function
})
main().catch(console.error)
+539
View File
@@ -0,0 +1,539 @@
Executes code in the server to control playwright.
You have access to a `page` object where you can call playwright methods on it to accomplish actions on the page.
You can also use `console.log` to examine the results of your actions.
You only have access to `page`, `context` and node.js globals. Do not try to import anything or setup handlers.
Your code should be stateless and do not depend on any state.
If you really want to attach listeners you should also detach them using a try finally block, to prevent memory leaks.
You can also create a new page via `context.newPage()` if you need to start fresh. You can then find that page by iteration over `context.pages()`:
```javascript
const page = context.pages().find((p) => p.url().includes('/some/path'))
```
## important rules
- NEVER call `page.waitForTimeout`, instead use `page.waitForSelector` or use a while loop that waits for a condition to be true.
- when a timeout error happen for example during navigation don't worry too much. try to get the snapshot of the page to see the current state, then continue without retrying if the state is what you expect. If the state is not what you expect, then you can retry the action.
- only call `page.close()` if the user asks you so or if you are in a test feedback loop and you know the user is not dependently interacting with the page (for example for debugging).
- always call `new_page` at the start of a conversation. later this page will be passed to the `execute` tool.
- In some rare cases you can also skip `new_page` tool, if the user asks you to instead use an existing page in the browser. You can set a page as default using `state.page = page`, `execute` calls will be passed this page in the scope later on.
- if running in localhost and some elements are difficult to target with locators you can update the source code to add `data-testid` attributes to elements you want to target. This will make running tests much easier later on. Also update the source markdown documents your are following if you do so.
- after every action call the tool `accessibility_snapshot` to get the page structure and understand what elements are available on the page
- after form submissions use `page.waitForLoadState('networkidle')` to ensure the page is fully loaded before proceeding
- sometimes when in localhost and using Vite you can encounter issues in the first page load, where a module is not found, because of updated optimization of the node_modules. In these cases you can try reloading the page 2 times and see if the issue resolves itself.
- for Google and GitHub login always use the Google account you have access to, already signed in
- if you are following a markdown document describing the steps to follow to test the website, update this document if you encounter unexpected behavior or if you can add information that would make the test faster, for example telling how to wait for actions that trigger loading states or to use a different timeout for specific actions.
## getting outputs of code execution
You can use `console.log` to print values you want to see in the tool call result
## using page.evaluate
you can execute client side JavaScript code using `page.evaluate()`
When executing code with `page.evaluate()`, return values directly from the evaluate function. Use `console.log()` outside of evaluate to display results:
```javascript
// Get data from the page by returning it
const title = await page.evaluate(() => document.title)
console.log('Page title:', title)
// Return multiple values as an object
const pageInfo = await page.evaluate(() => ({
url: window.location.href,
buttonCount: document.querySelectorAll('button').length,
readyState: document.readyState,
}))
console.log('Page URL:', pageInfo.url)
console.log('Number of buttons:', pageInfo.buttonCount)
console.log('Page ready state:', pageInfo.readyState)
```
## Finding Elements on the Page
you can use the tool accessibility_snapshot to get the page accessibility snapshot tree, which provides a structured view of the page's elements, including their roles and names. This is useful for understanding the page structure and finding elements to interact with.
Example accessibility snapshot result:
```md
- generic [active] [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e5]:
- 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]:
- /url: /docs/installation
- link "Components" [ref=e14] [cursor=pointer]:
- /url: /docs/components
- link "Blocks" [ref=e15] [cursor=pointer]:
- /url: /blocks
- link "Charts" [ref=e16] [cursor=pointer]:
- /url: /charts/area
- link "Themes" [ref=e17] [cursor=pointer]:
- /url: /themes
- link "Colors" [ref=e18] [cursor=pointer]:
- /url: /colors
```
Then you can use `page.locator(`aria-ref=${ref}`).describe(element);` to get an element with a specific `ref` and interact with it.
For example:
```javascript
const componentsLink = page
// Exact target element reference from the page snapshot
.locator('aria-ref=e14')
// Human-readable element description used to obtain permission to interact with the element
.describe('Components link')
componentsLink.click()
console.log('Clicked on Components link')
```
This approach is the preferred way to find elements on the page, as it allows you to use the structured information from the accessibility snapshot to interact with elements reliably.
You can also find `getByRole` to get elements on the page.
```javascript
// Then use the information from the snapshot to click elements
// For example, if snapshot shows: { "role": "button", "name": "Sign In" }
await page.getByRole('button', { name: 'Sign In' }).click()
// For a link with { "role": "link", "name": "About" }
await page.getByRole('link', { name: 'About' }).click()
// For a textbox with { "role": "textbox", "name": "Email" }
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com')
// For a heading with { "role": "heading", "name": "Welcome to Example.com" }
const headingText = await page
.getByRole('heading', { name: 'Welcome to Example.com' })
.textContent()
console.log('Heading text:', headingText)
```
### Complete Example: Find and Click Elements
```javascript
await page.getByRole('button', { name: 'Submit Form' }).click()
console.log('Clicked submit button')
await page.waitForLoadState('networkidle')
console.log('Form submitted successfully')
```
## Core Concepts
### Page and Context
In Playwright, automation happens through a `page` object (representing a browser tab) and `context` (representing a browser session with cookies, storage, etc.).
```javascript
// Assuming you have page and context already available
const page = await context.newPage()
```
### Element Selection
Playwright uses locators to find elements. The examples below show various selection methods:
```javascript
// By role (recommended)
await page.getByRole('button', { name: 'Submit' })
// By text
await page.getByText('Welcome')
// By placeholder
await page.getByPlaceholder('Enter email')
// By label
await page.getByLabel('Username')
// By test id
await page.getByTestId('submit-button')
// By CSS selector
await page.locator('.my-class')
// By XPath
await page.locator('//div[@class="content"]')
```
## Navigation
### Navigate to URL
```javascript
await page.goto('https://example.com')
// Wait for network idle (no requests for 500ms)
await page.goto('https://example.com', { waitUntil: 'networkidle' })
```
### Navigate Back/Forward
```javascript
// Go back to previous page
await page.goBack()
// Go forward to next page
await page.goForward()
```
## Screenshots
### Take Screenshot
```javascript
// Screenshot of viewport
await page.screenshot({ path: 'screenshot.png' })
// Full page screenshot
await page.screenshot({ path: 'fullpage.png', fullPage: true })
// Screenshot of specific element
const element = await page.getByRole('button', { name: 'Submit' })
await element.screenshot({ path: 'button.png' })
// Screenshot with custom dimensions
await page.setViewportSize({ width: 1280, height: 720 })
await page.screenshot({ path: 'custom-size.png' })
```
## Mouse Interactions
### Click Elements
```javascript
// Click by role
await page.getByRole('button', { name: 'Submit' }).click()
// Click at coordinates
await page.mouse.click(100, 200)
// Double click
await page.getByText('Double click me').dblclick()
// Right click
await page.getByText('Right click me').click({ button: 'right' })
// Click with modifiers
await page.getByText('Ctrl click me').click({ modifiers: ['Control'] })
```
### Hover
```javascript
// Hover over element
await page.getByText('Hover me').hover()
// Hover at coordinates
await page.mouse.move(100, 200)
```
## Keyboard Input
### Type Text
```javascript
// Type into input field
await page.getByLabel('Email').fill('user@example.com')
// Type character by character (simulates real typing)
await page.getByLabel('Email').type('user@example.com', { delay: 100 })
// Clear and type
await page.getByLabel('Email').clear()
await page.getByLabel('Email').fill('new@example.com')
```
### Press Keys
```javascript
// Press single key
await page.keyboard.press('Enter')
// Press key combination
await page.keyboard.press('Control+A')
// Press sequence of keys
await page.keyboard.press('Tab')
await page.keyboard.press('Tab')
await page.keyboard.press('Space')
// Common key shortcuts
await page.keyboard.press('Control+C') // Copy
await page.keyboard.press('Control+V') // Paste
await page.keyboard.press('Control+Z') // Undo
```
## Form Interactions
### Select Dropdown Options
```javascript
// Select by value
await page.selectOption('select#country', 'us')
// Select by label
await page.selectOption('select#country', { label: 'United States' })
// Select multiple options
await page.selectOption('select#colors', ['red', 'blue', 'green'])
// Get selected option
const selectedValue = await page.$eval('select#country', (el) => el.value)
```
### Checkboxes and Radio Buttons
```javascript
// Check checkbox
await page.getByLabel('I agree').check()
// Uncheck checkbox
await page.getByLabel('Subscribe').uncheck()
// Check if checked
const isChecked = await page.getByLabel('I agree').isChecked()
// Select radio button
await page.getByLabel('Option A').check()
```
## JavaScript Evaluation
### Execute JavaScript in Page Context
```javascript
// Evaluate simple expression
const result = await page.evaluate(() => 2 + 2)
// Access page variables
const pageTitle = await page.evaluate(() => document.title)
// Modify page
await page.evaluate(() => {
document.body.style.backgroundColor = 'red'
})
// Pass arguments to page context
const sum = await page.evaluate(([a, b]) => a + b, [5, 3])
// Work with elements
const elementText = await page.evaluate(
(el) => el.textContent,
await page.getByRole('heading'),
)
```
### Execute JavaScript on Element
```javascript
// Get element property
const href = await page.getByRole('link').evaluate((el) => el.href)
// Modify element
await page.getByRole('button').evaluate((el) => {
el.style.backgroundColor = 'green'
el.disabled = true
})
// Scroll element into view
await page.getByText('Section').evaluate((el) => el.scrollIntoView())
```
## File Handling
### File Upload
```javascript
// Upload single file
await page.getByLabel('Upload file').setInputFiles('/path/to/file.pdf')
// Upload multiple files
await page
.getByLabel('Upload files')
.setInputFiles(['/path/to/file1.pdf', '/path/to/file2.pdf'])
// Clear file input
await page.getByLabel('Upload file').setInputFiles([])
// For file inputs, use setInputFiles directly on the input element
// Find the file input element (often hidden)
await page.locator('input[type="file"]').setInputFiles('/path/to/file.pdf')
```
## Network Monitoring
### Check Network Activity
```javascript
// Wait for a specific request to complete and get its response
const response = await page.waitForResponse(
(response) =>
response.url().includes('/api/user') && response.status() === 200,
)
// Get response data
const responseBody = await response.json()
console.log('API response:', responseBody)
// Wait for specific request
const request = await page.waitForRequest('**/api/data')
console.log('Request URL:', request.url())
console.log('Request method:', request.method())
// Get all resources loaded by the page
const resources = await page.evaluate(() =>
performance.getEntriesByType('resource').map((r) => ({
name: r.name,
duration: r.duration,
size: r.transferSize,
})),
)
console.log('Page resources:', resources)
```
## Console Messages
### Capture Console Output
```javascript
// Console messages are automatically captured by the MCP implementation
// Use the console_logs tool to retrieve them
// To trigger console messages from the page:
await page.evaluate(() => {
console.log('This message will be captured')
console.error('This error will be captured')
console.warn('This warning will be captured')
})
// Then use the console_logs MCP tool to retrieve all captured messages
// The tool provides filtering by type and pagination
```
## Waiting
### Wait for Conditions
```javascript
// Wait for element to appear
await page.waitForSelector('.success-message')
// Wait for element to disappear
await page.waitForSelector('.loading', { state: 'hidden' })
await page.waitForURL(/github\.com.*\/pull/)
await page.waitForURL(/\/new-org/)
// Wait for text to appear
await page.waitForFunction(
(text) => document.body.textContent.includes(text),
'Success!',
)
// Wait for navigation
await page.waitForURL('**/success')
// Wait for page load
await page.waitForLoadState('networkidle')
// Wait for specific condition
await page.waitForFunction(
(text) => document.querySelector('.status')?.textContent === text,
'Ready',
)
```
### Wait for Text to Appear or Disappear
```javascript
// Wait for specific text to appear on the page
await page.getByText('Loading complete').first().waitFor({ state: 'visible' })
console.log('Loading complete text is now visible')
// Wait for text to disappear from the page
await page.getByText('Loading...').first().waitFor({ state: 'hidden' })
console.log('Loading text has disappeared')
// Wait for multiple conditions sequentially
// First wait for loading to disappear, then wait for success message
await page.getByText('Processing...').first().waitFor({ state: 'hidden' })
await page.getByText('Success!').first().waitFor({ state: 'visible' })
console.log('Processing finished and success message appeared')
// Example: Wait for error message to disappear before proceeding
await page
.getByText('Error: Please try again')
.first()
.waitFor({ state: 'hidden' })
await page.getByRole('button', { name: 'Submit' }).click()
// Example: Wait for confirmation text after form submission
await page.getByRole('button', { name: 'Save' }).click()
await page
.getByText('Your changes have been saved')
.first()
.waitFor({ state: 'visible' })
console.log('Save confirmed')
// Example: Wait for dynamic content to load
await page.getByRole('button', { name: 'Load More' }).click()
await page
.getByText('Loading more items...')
.first()
.waitFor({ state: 'visible' })
await page
.getByText('Loading more items...')
.first()
.waitFor({ state: 'hidden' })
console.log('Additional items loaded')
```
### Work with Frames
```javascript
// Get frame by name
const frame = page.frame('frameName')
// Get frame by URL
const frame = page.frame({ url: /frame\.html/ })
// Interact with frame content
await frame.getByText('In Frame').click()
// Get all frames
const frames = page.frames()
```
## Best Practices
### Reliable Selectors
```javascript
// Prefer user-facing attributes
await page.getByRole('button', { name: 'Submit' })
await page.getByLabel('Email')
await page.getByPlaceholder('Search...')
await page.getByText('Welcome')
// Use test IDs for complex cases
await page.getByTestId('complex-component')
// Avoid brittle selectors
// Bad: await page.locator('.btn-3842');
// Good: await page.getByRole('button', { name: 'Submit' });
```
@@ -0,0 +1,202 @@
- 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]:
- table [ref=e7]:
- rowgroup [ref=e8]:
- row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e9]:
- cell [ref=e10]:
- link [active] [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]:
- 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]:
- /url: front
- text: "|"
- link "comments" [ref=e19] [cursor=pointer]:
- /url: newcomments
- text: "|"
- link "ask" [ref=e20] [cursor=pointer]:
- /url: ask
- text: "|"
- link "show" [ref=e21] [cursor=pointer]:
- /url: show
- text: "|"
- link "jobs" [ref=e22] [cursor=pointer]:
- /url: jobs
- text: "|"
- link "submit" [ref=e23] [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]:
- /url: http://ycombinator.com
- generic [ref=e42]:
- text: (
- link "ycombinator.com" [ref=e43] [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]:
- cell [ref=e46]
- cell "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e47]:
- generic [ref=e48]:
- generic [ref=e49]: 57 points
- text: by
- link "pg" [ref=e50] [cursor=pointer]:
- /url: user?id=pg
- link "on Oct 9, 2006" [ref=e52] [cursor=pointer]:
- /url: item?id=1
- text: "|"
- link "hide" [ref=e54] [cursor=pointer]:
- /url: hide?id=1&goto=item%3Fid%3D1
- text: "|"
- link "past" [ref=e55] [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
- text: "|"
- link "3 comments" [ref=e57] [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]:
- 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]:
- /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:
- 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]:
- /url: user?id=pg
- link "on Oct 9, 2006" [ref=e108] [cursor=pointer]:
- /url: item?id=17
- generic [ref=e110]:
- text: "|"
- link [ref=e111] [cursor=pointer]:
- /url: "#15"
- text: parent
- link "[]" [ref=e112] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e115]:
- generic [ref=e116]: 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]:
- /url: user?id=dmon
- link "on Feb 25, 2007" [ref=e136] [cursor=pointer]:
- /url: item?id=1079
- generic [ref=e138]:
- text: "|"
- link [ref=e139] [cursor=pointer]:
- /url: "#15"
- text: root
- text: "|"
- link [ref=e140] [cursor=pointer]:
- /url: "#17"
- text: parent
- link "[]" [ref=e141] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e144]:
- generic [ref=e145]: 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]:
- 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]:
- /url: newsguidelines.html
- text: "|"
- link "FAQ" [ref=e165] [cursor=pointer]:
- /url: newsfaq.html
- text: "|"
- link "Lists" [ref=e166] [cursor=pointer]:
- /url: lists
- text: "|"
- link "API" [ref=e167] [cursor=pointer]:
- /url: https://github.com/HackerNews/API
- text: "|"
- link "Security" [ref=e168] [cursor=pointer]:
- /url: security.html
- text: "|"
- link "Legal" [ref=e169] [cursor=pointer]:
- /url: https://www.ycombinator.com/legal/
- text: "|"
- link "Apply to YC" [ref=e170] [cursor=pointer]:
- /url: https://www.ycombinator.com/apply/
- text: "|"
- link "Contact" [ref=e171] [cursor=pointer]:
- /url: mailto:hn@ycombinator.com
- generic [ref=e174]:
- text: "Search:"
- textbox [ref=e175]
@@ -0,0 +1,202 @@
- 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]:
- table [ref=e7]:
- rowgroup [ref=e8]:
- row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [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]:
- 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]:
- /url: front
- text: "|"
- link "comments" [ref=e19] [cursor=pointer]:
- /url: newcomments
- text: "|"
- link "ask" [ref=e20] [cursor=pointer]:
- /url: ask
- text: "|"
- link "show" [ref=e21] [cursor=pointer]:
- /url: show
- text: "|"
- link "jobs" [ref=e22] [cursor=pointer]:
- /url: jobs
- text: "|"
- link "submit" [ref=e23] [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]:
- /url: http://ycombinator.com
- generic [ref=e42]:
- text: (
- link "ycombinator.com" [ref=e43] [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]:
- cell [ref=e46]
- cell "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e47]:
- generic [ref=e48]:
- generic [ref=e49]: 57 points
- text: by
- link "pg" [ref=e50] [cursor=pointer]:
- /url: user?id=pg
- link "on Oct 9, 2006" [ref=e52] [cursor=pointer]:
- /url: item?id=1
- text: "|"
- link "hide" [ref=e54] [cursor=pointer]:
- /url: hide?id=1&goto=item%3Fid%3D1
- text: "|"
- link "past" [ref=e55] [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
- text: "|"
- link "3 comments" [ref=e57] [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]:
- 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]:
- /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:
- 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]:
- /url: user?id=pg
- link "on Oct 9, 2006" [ref=e108] [cursor=pointer]:
- /url: item?id=17
- generic [ref=e110]:
- text: "|"
- link [ref=e111] [cursor=pointer]:
- /url: "#15"
- text: parent
- link "[]" [ref=e112] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e115]:
- generic [ref=e116]: 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]:
- /url: user?id=dmon
- link "on Feb 25, 2007" [ref=e136] [cursor=pointer]:
- /url: item?id=1079
- generic [ref=e138]:
- text: "|"
- link [ref=e139] [cursor=pointer]:
- /url: "#15"
- text: root
- text: "|"
- link [ref=e140] [cursor=pointer]:
- /url: "#17"
- text: parent
- link "[]" [ref=e141] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e144]:
- generic [ref=e145]: 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]:
- 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]:
- /url: newsguidelines.html
- text: "|"
- link "FAQ" [ref=e165] [cursor=pointer]:
- /url: newsfaq.html
- text: "|"
- link "Lists" [ref=e166] [cursor=pointer]:
- /url: lists
- text: "|"
- link "API" [ref=e167] [cursor=pointer]:
- /url: https://github.com/HackerNews/API
- text: "|"
- link "Security" [ref=e168] [cursor=pointer]:
- /url: security.html
- text: "|"
- link "Legal" [ref=e169] [cursor=pointer]:
- /url: https://www.ycombinator.com/legal/
- text: "|"
- link "Apply to YC" [ref=e170] [cursor=pointer]:
- /url: https://www.ycombinator.com/apply/
- text: "|"
- link "Contact" [ref=e171] [cursor=pointer]:
- /url: mailto:hn@ycombinator.com
- generic [ref=e174]:
- text: "Search:"
- textbox [ref=e175]
@@ -0,0 +1,202 @@
- 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]:
- table [ref=e7]:
- rowgroup [ref=e8]:
- row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [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]:
- generic [ref=e14]:
- link "Hacker News" [active] [ref=e16] [cursor=pointer]:
- /url: news
- link "new" [ref=e17] [cursor=pointer]:
- /url: newest
- text: "|"
- link "past" [ref=e18] [cursor=pointer]:
- /url: front
- text: "|"
- link "comments" [ref=e19] [cursor=pointer]:
- /url: newcomments
- text: "|"
- link "ask" [ref=e20] [cursor=pointer]:
- /url: ask
- text: "|"
- link "show" [ref=e21] [cursor=pointer]:
- /url: show
- text: "|"
- link "jobs" [ref=e22] [cursor=pointer]:
- /url: jobs
- text: "|"
- link "submit" [ref=e23] [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]:
- /url: http://ycombinator.com
- generic [ref=e42]:
- text: (
- link "ycombinator.com" [ref=e43] [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]:
- cell [ref=e46]
- cell "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e47]:
- generic [ref=e48]:
- generic [ref=e49]: 57 points
- text: by
- link "pg" [ref=e50] [cursor=pointer]:
- /url: user?id=pg
- link "on Oct 9, 2006" [ref=e52] [cursor=pointer]:
- /url: item?id=1
- text: "|"
- link "hide" [ref=e54] [cursor=pointer]:
- /url: hide?id=1&goto=item%3Fid%3D1
- text: "|"
- link "past" [ref=e55] [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
- text: "|"
- link "3 comments" [ref=e57] [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]:
- 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]:
- /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:
- 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]:
- /url: user?id=pg
- link "on Oct 9, 2006" [ref=e108] [cursor=pointer]:
- /url: item?id=17
- generic [ref=e110]:
- text: "|"
- link [ref=e111] [cursor=pointer]:
- /url: "#15"
- text: parent
- link "[]" [ref=e112] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e115]:
- generic [ref=e116]: 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]:
- /url: user?id=dmon
- link "on Feb 25, 2007" [ref=e136] [cursor=pointer]:
- /url: item?id=1079
- generic [ref=e138]:
- text: "|"
- link [ref=e139] [cursor=pointer]:
- /url: "#15"
- text: root
- text: "|"
- link [ref=e140] [cursor=pointer]:
- /url: "#17"
- text: parent
- link "[]" [ref=e141] [cursor=pointer]:
- /url: javascript:void(0)
- generic [ref=e144]:
- generic [ref=e145]: 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]:
- 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]:
- /url: newsguidelines.html
- text: "|"
- link "FAQ" [ref=e165] [cursor=pointer]:
- /url: newsfaq.html
- text: "|"
- link "Lists" [ref=e166] [cursor=pointer]:
- /url: lists
- text: "|"
- link "API" [ref=e167] [cursor=pointer]:
- /url: https://github.com/HackerNews/API
- text: "|"
- link "Security" [ref=e168] [cursor=pointer]:
- /url: security.html
- text: "|"
- link "Legal" [ref=e169] [cursor=pointer]:
- /url: https://www.ycombinator.com/legal/
- text: "|"
- link "Apply to YC" [ref=e170] [cursor=pointer]:
- /url: https://www.ycombinator.com/apply/
- text: "|"
- link "Contact" [ref=e171] [cursor=pointer]:
- /url: mailto:hn@ycombinator.com
- generic [ref=e174]:
- text: "Search:"
- textbox [ref=e175]
@@ -0,0 +1,553 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e5]:
- 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]:
- /url: /docs/installation
- link "Components" [ref=e14] [cursor=pointer]:
- /url: /docs/components
- link "Blocks" [ref=e15] [cursor=pointer]:
- /url: /blocks
- link "Charts" [ref=e16] [cursor=pointer]:
- /url: /charts/area
- link "Themes" [ref=e17] [cursor=pointer]:
- /url: /themes
- link "Colors" [ref=e18] [cursor=pointer]:
- /url: /colors
- generic [ref=e19]:
- button "Search documentation... ⌘ K" [ref=e21]:
- generic [ref=e22]: Search documentation...
- generic [ref=e23]:
- generic: ⌘
- generic: K
- link "91.7k" [ref=e26] [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
- img
- button "Toggle theme" [ref=e36]:
- 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
- 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]:
- /url: /docs/installation
- link "View Components" [ref=e58] [cursor=pointer]:
- /url: /docs/components
- generic [ref=e60]:
- generic [ref=e65]:
- link "Examples" [ref=e66] [cursor=pointer]:
- /url: /
- link "Dashboard" [ref=e67] [cursor=pointer]:
- /url: /examples/dashboard
- link "Tasks" [ref=e68] [cursor=pointer]:
- /url: /examples/tasks
- link "Playground" [ref=e69] [cursor=pointer]:
- /url: /examples/playground
- link "Authentication" [ref=e70] [cursor=pointer]:
- /url: /examples/authentication
- generic [ref=e71]:
- generic [ref=e72]: Theme
- combobox "Theme" [ref=e73]:
- generic [ref=e74]: "Theme:"
- generic: Default
- 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]:
- 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]:
- 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
- generic:
- 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]:
- text: Built by
- link "shadcn" [ref=e723] [cursor=pointer]:
- /url: https://twitter.com/shadcn
- text: at
- link "Vercel" [ref=e724] [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]:
- /url: https://github.com/shadcn-ui/ui
- text: .
- region "Notifications alt+T"
- alert [ref=e728]
- generic [ref=e729]: Mon
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
testTimeout: 60000, // 60 seconds for Chrome startup
hookTimeout: 30000,
},
})